feat: expand platform coverage with 8 new adaptors, 7 new CLI agents, and OpenCode skill tools
Phase 1 - OpenCode Integration: - Add OpenCodeAdaptor with directory-based packaging and dual-format YAML frontmatter - Kebab-case name validation matching OpenCode's regex spec Phase 2 - OpenAI-Compatible LLM Platforms: - Extract OpenAICompatibleAdaptor base class from MiniMax (shared format/package/upload/enhance) - Refactor MiniMax to ~20 lines of constants inheriting from base - Add 6 new LLM adaptors: Kimi, DeepSeek, Qwen, OpenRouter, Together AI, Fireworks AI - All use OpenAI-compatible API with platform-specific constants Phase 3 - CLI Agent Expansion: - Add 7 new install-agent paths: roo, cline, aider, bolt, kilo, continue, kimi-code - Total agents: 11 -> 18 Phase 4 - Advanced Features: - OpenCode skill splitter (auto-split large docs into focused sub-skills with router) - Bi-directional skill format converter (import/export between OpenCode and any platform) - GitHub Actions template for automated skill updates Totals: 12 --target platforms, 18 --agent paths, 2915 tests passing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,9 @@
|
||||
Multi-LLM Adaptor Registry
|
||||
|
||||
Provides factory function to get platform-specific adaptors for skill generation.
|
||||
Supports Claude AI, Google Gemini, OpenAI ChatGPT, MiniMax AI, and generic Markdown export.
|
||||
Supports Claude AI, Google Gemini, OpenAI ChatGPT, MiniMax AI, OpenCode,
|
||||
Kimi, DeepSeek, Qwen, OpenRouter, Together AI, Fireworks AI,
|
||||
and generic Markdown export.
|
||||
"""
|
||||
|
||||
from .base import SkillAdaptor, SkillMetadata
|
||||
@@ -74,6 +76,41 @@ try:
|
||||
except ImportError:
|
||||
MiniMaxAdaptor = None
|
||||
|
||||
try:
|
||||
from .opencode import OpenCodeAdaptor
|
||||
except ImportError:
|
||||
OpenCodeAdaptor = None
|
||||
|
||||
try:
|
||||
from .kimi import KimiAdaptor
|
||||
except ImportError:
|
||||
KimiAdaptor = None
|
||||
|
||||
try:
|
||||
from .deepseek import DeepSeekAdaptor
|
||||
except ImportError:
|
||||
DeepSeekAdaptor = None
|
||||
|
||||
try:
|
||||
from .qwen import QwenAdaptor
|
||||
except ImportError:
|
||||
QwenAdaptor = None
|
||||
|
||||
try:
|
||||
from .openrouter import OpenRouterAdaptor
|
||||
except ImportError:
|
||||
OpenRouterAdaptor = None
|
||||
|
||||
try:
|
||||
from .together import TogetherAdaptor
|
||||
except ImportError:
|
||||
TogetherAdaptor = None
|
||||
|
||||
try:
|
||||
from .fireworks import FireworksAdaptor
|
||||
except ImportError:
|
||||
FireworksAdaptor = None
|
||||
|
||||
|
||||
# Registry of available adaptors
|
||||
ADAPTORS: dict[str, type[SkillAdaptor]] = {}
|
||||
@@ -105,6 +142,20 @@ if PineconeAdaptor:
|
||||
ADAPTORS["pinecone"] = PineconeAdaptor
|
||||
if MiniMaxAdaptor:
|
||||
ADAPTORS["minimax"] = MiniMaxAdaptor
|
||||
if OpenCodeAdaptor:
|
||||
ADAPTORS["opencode"] = OpenCodeAdaptor
|
||||
if KimiAdaptor:
|
||||
ADAPTORS["kimi"] = KimiAdaptor
|
||||
if DeepSeekAdaptor:
|
||||
ADAPTORS["deepseek"] = DeepSeekAdaptor
|
||||
if QwenAdaptor:
|
||||
ADAPTORS["qwen"] = QwenAdaptor
|
||||
if OpenRouterAdaptor:
|
||||
ADAPTORS["openrouter"] = OpenRouterAdaptor
|
||||
if TogetherAdaptor:
|
||||
ADAPTORS["together"] = TogetherAdaptor
|
||||
if FireworksAdaptor:
|
||||
ADAPTORS["fireworks"] = FireworksAdaptor
|
||||
|
||||
|
||||
def get_adaptor(platform: str, config: dict = None) -> SkillAdaptor:
|
||||
@@ -112,7 +163,9 @@ def get_adaptor(platform: str, config: dict = None) -> SkillAdaptor:
|
||||
Factory function to get platform-specific adaptor instance.
|
||||
|
||||
Args:
|
||||
platform: Platform identifier ('claude', 'gemini', 'openai', 'minimax', 'markdown')
|
||||
platform: Platform identifier (e.g., 'claude', 'gemini', 'openai', 'minimax',
|
||||
'opencode', 'kimi', 'deepseek', 'qwen', 'openrouter', 'together',
|
||||
'fireworks', 'markdown')
|
||||
config: Optional platform-specific configuration
|
||||
|
||||
Returns:
|
||||
|
||||
19
src/skill_seekers/cli/adaptors/deepseek.py
Normal file
19
src/skill_seekers/cli/adaptors/deepseek.py
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
DeepSeek AI Adaptor
|
||||
|
||||
OpenAI-compatible LLM platform adaptor for DeepSeek.
|
||||
"""
|
||||
|
||||
from .openai_compatible import OpenAICompatibleAdaptor
|
||||
|
||||
|
||||
class DeepSeekAdaptor(OpenAICompatibleAdaptor):
|
||||
"""DeepSeek AI platform adaptor."""
|
||||
|
||||
PLATFORM = "deepseek"
|
||||
PLATFORM_NAME = "DeepSeek AI"
|
||||
DEFAULT_API_ENDPOINT = "https://api.deepseek.com/v1"
|
||||
DEFAULT_MODEL = "deepseek-chat"
|
||||
ENV_VAR_NAME = "DEEPSEEK_API_KEY"
|
||||
PLATFORM_URL = "https://platform.deepseek.com/"
|
||||
19
src/skill_seekers/cli/adaptors/fireworks.py
Normal file
19
src/skill_seekers/cli/adaptors/fireworks.py
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Fireworks AI Adaptor
|
||||
|
||||
OpenAI-compatible LLM platform adaptor for Fireworks AI.
|
||||
"""
|
||||
|
||||
from .openai_compatible import OpenAICompatibleAdaptor
|
||||
|
||||
|
||||
class FireworksAdaptor(OpenAICompatibleAdaptor):
|
||||
"""Fireworks AI platform adaptor."""
|
||||
|
||||
PLATFORM = "fireworks"
|
||||
PLATFORM_NAME = "Fireworks AI"
|
||||
DEFAULT_API_ENDPOINT = "https://api.fireworks.ai/inference/v1"
|
||||
DEFAULT_MODEL = "accounts/fireworks/models/llama-v3p1-70b-instruct"
|
||||
ENV_VAR_NAME = "FIREWORKS_API_KEY"
|
||||
PLATFORM_URL = "https://fireworks.ai/"
|
||||
19
src/skill_seekers/cli/adaptors/kimi.py
Normal file
19
src/skill_seekers/cli/adaptors/kimi.py
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Kimi (Moonshot AI) Adaptor
|
||||
|
||||
OpenAI-compatible LLM platform adaptor for Kimi/Moonshot AI.
|
||||
"""
|
||||
|
||||
from .openai_compatible import OpenAICompatibleAdaptor
|
||||
|
||||
|
||||
class KimiAdaptor(OpenAICompatibleAdaptor):
|
||||
"""Kimi (Moonshot AI) platform adaptor."""
|
||||
|
||||
PLATFORM = "kimi"
|
||||
PLATFORM_NAME = "Kimi (Moonshot AI)"
|
||||
DEFAULT_API_ENDPOINT = "https://api.moonshot.cn/v1"
|
||||
DEFAULT_MODEL = "moonshot-v1-128k"
|
||||
ENV_VAR_NAME = "MOONSHOT_API_KEY"
|
||||
PLATFORM_URL = "https://platform.moonshot.cn/"
|
||||
@@ -2,502 +2,19 @@
|
||||
"""
|
||||
MiniMax AI Adaptor
|
||||
|
||||
Implements platform-specific handling for MiniMax AI skills.
|
||||
OpenAI-compatible LLM platform adaptor for MiniMax AI.
|
||||
Uses MiniMax's OpenAI-compatible API for AI enhancement with M2.7 model.
|
||||
"""
|
||||
|
||||
import json
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .base import SkillAdaptor, SkillMetadata
|
||||
from skill_seekers.cli.arguments.common import DEFAULT_CHUNK_TOKENS, DEFAULT_CHUNK_OVERLAP_TOKENS
|
||||
from .openai_compatible import OpenAICompatibleAdaptor
|
||||
|
||||
|
||||
class MiniMaxAdaptor(SkillAdaptor):
|
||||
"""
|
||||
MiniMax AI platform adaptor.
|
||||
|
||||
Handles:
|
||||
- System instructions format (plain text, no YAML frontmatter)
|
||||
- ZIP packaging with knowledge files
|
||||
- AI enhancement using MiniMax-M2.7
|
||||
"""
|
||||
class MiniMaxAdaptor(OpenAICompatibleAdaptor):
|
||||
"""MiniMax AI platform adaptor."""
|
||||
|
||||
PLATFORM = "minimax"
|
||||
PLATFORM_NAME = "MiniMax AI"
|
||||
DEFAULT_API_ENDPOINT = "https://api.minimax.io/v1"
|
||||
|
||||
def format_skill_md(self, skill_dir: Path, metadata: SkillMetadata) -> str:
|
||||
"""
|
||||
Format SKILL.md as system instructions for MiniMax AI.
|
||||
|
||||
MiniMax uses OpenAI-compatible chat completions, so instructions
|
||||
are formatted as clear system prompts without YAML frontmatter.
|
||||
|
||||
Args:
|
||||
skill_dir: Path to skill directory
|
||||
metadata: Skill metadata
|
||||
|
||||
Returns:
|
||||
Formatted instructions for MiniMax AI
|
||||
"""
|
||||
existing_content = self._read_existing_content(skill_dir)
|
||||
|
||||
if existing_content and len(existing_content) > 100:
|
||||
content_body = f"""You are an expert assistant for {metadata.name}.
|
||||
|
||||
{metadata.description}
|
||||
|
||||
Use the attached knowledge files to provide accurate, detailed answers about {metadata.name}.
|
||||
|
||||
{existing_content}
|
||||
|
||||
## How to Assist Users
|
||||
|
||||
When users ask questions:
|
||||
1. Search the knowledge files for relevant information
|
||||
2. Provide clear, practical answers with code examples
|
||||
3. Reference specific documentation sections when helpful
|
||||
4. Be concise but thorough
|
||||
|
||||
Always prioritize accuracy by consulting the knowledge base before responding."""
|
||||
else:
|
||||
content_body = f"""You are an expert assistant for {metadata.name}.
|
||||
|
||||
{metadata.description}
|
||||
|
||||
## Your Knowledge Base
|
||||
|
||||
You have access to comprehensive documentation files about {metadata.name}. Use these files to provide accurate answers to user questions.
|
||||
|
||||
{self._generate_toc(skill_dir)}
|
||||
|
||||
## Quick Reference
|
||||
|
||||
{self._extract_quick_reference(skill_dir)}
|
||||
|
||||
## How to Assist Users
|
||||
|
||||
When users ask questions about {metadata.name}:
|
||||
|
||||
1. **Search the knowledge files** - Find relevant information in the documentation
|
||||
2. **Provide code examples** - Include practical, working code snippets
|
||||
3. **Reference documentation** - Cite specific sections when helpful
|
||||
4. **Be practical** - Focus on real-world usage and best practices
|
||||
5. **Stay accurate** - Always verify information against the knowledge base
|
||||
|
||||
## Response Guidelines
|
||||
|
||||
- Keep answers clear and concise
|
||||
- Use proper code formatting with language tags
|
||||
- Provide both simple and detailed explanations as needed
|
||||
- Suggest related topics when relevant
|
||||
- Admit when information isn't in the knowledge base
|
||||
|
||||
Always prioritize accuracy by consulting the attached documentation files before responding."""
|
||||
|
||||
return content_body
|
||||
|
||||
def package(
|
||||
self,
|
||||
skill_dir: Path,
|
||||
output_path: Path,
|
||||
enable_chunking: bool = False,
|
||||
chunk_max_tokens: int = DEFAULT_CHUNK_TOKENS,
|
||||
preserve_code_blocks: bool = True,
|
||||
chunk_overlap_tokens: int = DEFAULT_CHUNK_OVERLAP_TOKENS,
|
||||
) -> Path:
|
||||
"""
|
||||
Package skill into ZIP file for MiniMax AI.
|
||||
|
||||
Creates MiniMax-compatible structure:
|
||||
- system_instructions.txt (main instructions)
|
||||
- knowledge_files/*.md (reference files)
|
||||
- minimax_metadata.json (skill metadata)
|
||||
|
||||
Args:
|
||||
skill_dir: Path to skill directory
|
||||
output_path: Output path/filename for ZIP
|
||||
|
||||
Returns:
|
||||
Path to created ZIP file
|
||||
"""
|
||||
skill_dir = Path(skill_dir)
|
||||
output_path = Path(output_path)
|
||||
|
||||
if output_path.is_dir() or str(output_path).endswith("/"):
|
||||
output_path = Path(output_path) / f"{skill_dir.name}-minimax.zip"
|
||||
elif not str(output_path).endswith(".zip") and not str(output_path).endswith(
|
||||
"-minimax.zip"
|
||||
):
|
||||
output_str = str(output_path).replace(".zip", "-minimax.zip")
|
||||
if not output_str.endswith(".zip"):
|
||||
output_str += ".zip"
|
||||
output_path = Path(output_str)
|
||||
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||
skill_md = skill_dir / "SKILL.md"
|
||||
if skill_md.exists():
|
||||
instructions = skill_md.read_text(encoding="utf-8")
|
||||
zf.writestr("system_instructions.txt", instructions)
|
||||
|
||||
refs_dir = skill_dir / "references"
|
||||
if refs_dir.exists():
|
||||
for ref_file in refs_dir.rglob("*.md"):
|
||||
if ref_file.is_file() and not ref_file.name.startswith("."):
|
||||
arcname = f"knowledge_files/{ref_file.name}"
|
||||
zf.write(ref_file, arcname)
|
||||
|
||||
metadata = {
|
||||
"platform": "minimax",
|
||||
"name": skill_dir.name,
|
||||
"version": "1.0.0",
|
||||
"created_with": "skill-seekers",
|
||||
"model": "MiniMax-M2.7",
|
||||
"api_base": self.DEFAULT_API_ENDPOINT,
|
||||
}
|
||||
|
||||
zf.writestr("minimax_metadata.json", json.dumps(metadata, indent=2))
|
||||
|
||||
return output_path
|
||||
|
||||
def upload(self, package_path: Path, api_key: str, **kwargs) -> dict[str, Any]:
|
||||
"""
|
||||
Upload packaged skill to MiniMax AI.
|
||||
|
||||
MiniMax uses an OpenAI-compatible chat completion API.
|
||||
This method validates the package and prepares it for use
|
||||
with the MiniMax API.
|
||||
|
||||
Args:
|
||||
package_path: Path to skill ZIP file
|
||||
api_key: MiniMax API key
|
||||
**kwargs: Additional arguments (model, etc.)
|
||||
|
||||
Returns:
|
||||
Dictionary with upload result
|
||||
"""
|
||||
package_path = Path(package_path)
|
||||
if not package_path.exists():
|
||||
return {
|
||||
"success": False,
|
||||
"skill_id": None,
|
||||
"url": None,
|
||||
"message": f"File not found: {package_path}",
|
||||
}
|
||||
|
||||
if package_path.suffix != ".zip":
|
||||
return {
|
||||
"success": False,
|
||||
"skill_id": None,
|
||||
"url": None,
|
||||
"message": f"Not a ZIP file: {package_path}",
|
||||
}
|
||||
|
||||
try:
|
||||
from openai import OpenAI, APITimeoutError, APIConnectionError
|
||||
except ImportError:
|
||||
return {
|
||||
"success": False,
|
||||
"skill_id": None,
|
||||
"url": None,
|
||||
"message": "openai library not installed. Run: pip install openai",
|
||||
}
|
||||
|
||||
try:
|
||||
import tempfile
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
with zipfile.ZipFile(package_path, "r") as zf:
|
||||
zf.extractall(temp_dir)
|
||||
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
instructions_file = temp_path / "system_instructions.txt"
|
||||
if not instructions_file.exists():
|
||||
return {
|
||||
"success": False,
|
||||
"skill_id": None,
|
||||
"url": None,
|
||||
"message": "Invalid package: system_instructions.txt not found",
|
||||
}
|
||||
|
||||
instructions = instructions_file.read_text(encoding="utf-8")
|
||||
|
||||
metadata_file = temp_path / "minimax_metadata.json"
|
||||
skill_name = package_path.stem
|
||||
model = kwargs.get("model", "MiniMax-M2.7")
|
||||
|
||||
if metadata_file.exists():
|
||||
with open(metadata_file) as f:
|
||||
metadata = json.load(f)
|
||||
skill_name = metadata.get("name", skill_name)
|
||||
model = metadata.get("model", model)
|
||||
|
||||
knowledge_dir = temp_path / "knowledge_files"
|
||||
knowledge_count = 0
|
||||
if knowledge_dir.exists():
|
||||
knowledge_count = len(list(knowledge_dir.glob("*.md")))
|
||||
|
||||
client = OpenAI(
|
||||
api_key=api_key,
|
||||
base_url=self.DEFAULT_API_ENDPOINT,
|
||||
)
|
||||
|
||||
client.chat.completions.create(
|
||||
model=model,
|
||||
messages=[
|
||||
{"role": "system", "content": instructions},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Confirm you are ready to assist with {skill_name}. Reply briefly.",
|
||||
},
|
||||
],
|
||||
temperature=0.3,
|
||||
max_tokens=100,
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"skill_id": None,
|
||||
"url": "https://platform.minimaxi.com/",
|
||||
"message": f"Skill '{skill_name}' validated with MiniMax {model} ({knowledge_count} knowledge files)",
|
||||
}
|
||||
|
||||
except APITimeoutError:
|
||||
return {
|
||||
"success": False,
|
||||
"skill_id": None,
|
||||
"url": None,
|
||||
"message": "Upload timed out. Try again.",
|
||||
}
|
||||
except APIConnectionError:
|
||||
return {
|
||||
"success": False,
|
||||
"skill_id": None,
|
||||
"url": None,
|
||||
"message": "Connection error. Check your internet connection.",
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"skill_id": None,
|
||||
"url": None,
|
||||
"message": f"Upload failed: {str(e)}",
|
||||
}
|
||||
|
||||
def validate_api_key(self, api_key: str) -> bool:
|
||||
"""
|
||||
Validate MiniMax API key format.
|
||||
|
||||
MiniMax API keys are opaque strings. We only check for
|
||||
a non-empty key with a reasonable minimum length.
|
||||
|
||||
Args:
|
||||
api_key: API key to validate
|
||||
|
||||
Returns:
|
||||
True if key format appears valid
|
||||
"""
|
||||
key = api_key.strip()
|
||||
return len(key) > 10
|
||||
|
||||
def get_env_var_name(self) -> str:
|
||||
"""
|
||||
Get environment variable name for MiniMax API key.
|
||||
|
||||
Returns:
|
||||
'MINIMAX_API_KEY'
|
||||
"""
|
||||
return "MINIMAX_API_KEY"
|
||||
|
||||
def supports_enhancement(self) -> bool:
|
||||
"""
|
||||
MiniMax supports AI enhancement via MiniMax-M2.7.
|
||||
|
||||
Returns:
|
||||
True
|
||||
"""
|
||||
return True
|
||||
|
||||
def enhance(self, skill_dir: Path, api_key: str) -> bool:
|
||||
"""
|
||||
Enhance SKILL.md using MiniMax-M2.7 API.
|
||||
|
||||
Uses MiniMax's OpenAI-compatible API endpoint for enhancement.
|
||||
|
||||
Args:
|
||||
skill_dir: Path to skill directory
|
||||
api_key: MiniMax API key
|
||||
|
||||
Returns:
|
||||
True if enhancement succeeded
|
||||
"""
|
||||
try:
|
||||
from openai import OpenAI
|
||||
except ImportError:
|
||||
print("❌ Error: openai package not installed")
|
||||
print("Install with: pip install openai")
|
||||
return False
|
||||
|
||||
skill_dir = Path(skill_dir)
|
||||
references_dir = skill_dir / "references"
|
||||
skill_md_path = skill_dir / "SKILL.md"
|
||||
|
||||
print("📖 Reading reference documentation...")
|
||||
references = self._read_reference_files(references_dir)
|
||||
|
||||
if not references:
|
||||
print("❌ No reference files found to analyze")
|
||||
return False
|
||||
|
||||
print(f" ✓ Read {len(references)} reference files")
|
||||
total_size = sum(len(c) for c in references.values())
|
||||
print(f" ✓ Total size: {total_size:,} characters\n")
|
||||
|
||||
current_skill_md = None
|
||||
if skill_md_path.exists():
|
||||
current_skill_md = skill_md_path.read_text(encoding="utf-8")
|
||||
print(f" ℹ Found existing SKILL.md ({len(current_skill_md)} chars)")
|
||||
else:
|
||||
print(" ℹ No existing SKILL.md, will create new one")
|
||||
|
||||
prompt = self._build_enhancement_prompt(skill_dir.name, references, current_skill_md)
|
||||
|
||||
print("\n🤖 Asking MiniMax-M2.7 to enhance SKILL.md...")
|
||||
print(f" Input: {len(prompt):,} characters")
|
||||
|
||||
try:
|
||||
client = OpenAI(
|
||||
api_key=api_key,
|
||||
base_url="https://api.minimax.io/v1",
|
||||
)
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model="MiniMax-M2.7",
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are an expert technical writer creating system instructions for MiniMax AI.",
|
||||
},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
temperature=0.3,
|
||||
max_tokens=4096,
|
||||
)
|
||||
|
||||
enhanced_content = response.choices[0].message.content
|
||||
print(f" ✓ Generated enhanced SKILL.md ({len(enhanced_content)} chars)\n")
|
||||
|
||||
if skill_md_path.exists():
|
||||
backup_path = skill_md_path.with_suffix(".md.backup")
|
||||
skill_md_path.rename(backup_path)
|
||||
print(f" 💾 Backed up original to: {backup_path.name}")
|
||||
|
||||
skill_md_path.write_text(enhanced_content, encoding="utf-8")
|
||||
print(" ✅ Saved enhanced SKILL.md")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error calling MiniMax API: {e}")
|
||||
return False
|
||||
|
||||
def _read_reference_files(
|
||||
self, references_dir: Path, max_chars: int = 200000
|
||||
) -> dict[str, str]:
|
||||
"""
|
||||
Read reference markdown files from skill directory.
|
||||
|
||||
Args:
|
||||
references_dir: Path to references directory
|
||||
max_chars: Maximum total characters to read
|
||||
|
||||
Returns:
|
||||
Dictionary mapping filename to content
|
||||
"""
|
||||
if not references_dir.exists():
|
||||
return {}
|
||||
|
||||
references = {}
|
||||
total_chars = 0
|
||||
|
||||
for ref_file in sorted(references_dir.glob("*.md")):
|
||||
if total_chars >= max_chars:
|
||||
break
|
||||
|
||||
try:
|
||||
content = ref_file.read_text(encoding="utf-8")
|
||||
if len(content) > 30000:
|
||||
content = content[:30000] + "\n\n...(truncated)"
|
||||
|
||||
references[ref_file.name] = content
|
||||
total_chars += len(content)
|
||||
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Could not read {ref_file.name}: {e}")
|
||||
|
||||
return references
|
||||
|
||||
def _build_enhancement_prompt(
|
||||
self, skill_name: str, references: dict[str, str], current_skill_md: str = None
|
||||
) -> str:
|
||||
"""
|
||||
Build MiniMax API prompt for enhancement.
|
||||
|
||||
Args:
|
||||
skill_name: Name of the skill
|
||||
references: Dictionary of reference content
|
||||
current_skill_md: Existing SKILL.md content (optional)
|
||||
|
||||
Returns:
|
||||
Enhancement prompt for MiniMax-M2.7
|
||||
"""
|
||||
prompt = f"""You are creating system instructions for a MiniMax AI assistant about: {skill_name}
|
||||
|
||||
I've scraped documentation and organized it into reference files. Your job is to create EXCELLENT system instructions that will help the assistant use this documentation effectively.
|
||||
|
||||
CURRENT INSTRUCTIONS:
|
||||
{"```" if current_skill_md else "(none - create from scratch)"}
|
||||
{current_skill_md or "No existing instructions"}
|
||||
{"```" if current_skill_md else ""}
|
||||
|
||||
REFERENCE DOCUMENTATION:
|
||||
"""
|
||||
|
||||
for filename, content in references.items():
|
||||
prompt += f"\n\n## {filename}\n```markdown\n{content[:30000]}\n```\n"
|
||||
|
||||
prompt += """
|
||||
|
||||
YOUR TASK:
|
||||
Create enhanced system instructions that include:
|
||||
|
||||
1. **Clear role definition** - "You are an expert assistant for [topic]"
|
||||
2. **Knowledge base description** - What documentation is attached
|
||||
3. **Excellent Quick Reference** - Extract 5-10 of the BEST, most practical code examples from the reference docs
|
||||
- Choose SHORT, clear examples that demonstrate common tasks
|
||||
- Include both simple and intermediate examples
|
||||
- Annotate examples with clear descriptions
|
||||
- Use proper language tags (cpp, python, javascript, json, etc.)
|
||||
4. **Response guidelines** - How the assistant should help users
|
||||
5. **Search strategy** - How to find information in the knowledge base
|
||||
6. **DO NOT use YAML frontmatter** - This is plain text instructions
|
||||
|
||||
IMPORTANT:
|
||||
- Extract REAL examples from the reference docs, don't make them up
|
||||
- Prioritize SHORT, clear examples (5-20 lines max)
|
||||
- Make it actionable and practical
|
||||
- Write clear, direct instructions
|
||||
- Focus on how the assistant should behave and respond
|
||||
- NO YAML frontmatter (no --- blocks)
|
||||
|
||||
OUTPUT:
|
||||
Return ONLY the complete system instructions as plain text.
|
||||
"""
|
||||
|
||||
return prompt
|
||||
DEFAULT_MODEL = "MiniMax-M2.7"
|
||||
ENV_VAR_NAME = "MINIMAX_API_KEY"
|
||||
PLATFORM_URL = "https://platform.minimaxi.com/"
|
||||
|
||||
431
src/skill_seekers/cli/adaptors/openai_compatible.py
Normal file
431
src/skill_seekers/cli/adaptors/openai_compatible.py
Normal file
@@ -0,0 +1,431 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
OpenAI-Compatible Base Adaptor
|
||||
|
||||
Shared base class for all LLM platforms that use OpenAI-compatible APIs.
|
||||
Subclasses only need to override platform constants (~15 lines each).
|
||||
"""
|
||||
|
||||
import json
|
||||
import tempfile
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .base import SkillAdaptor, SkillMetadata
|
||||
from skill_seekers.cli.arguments.common import DEFAULT_CHUNK_TOKENS, DEFAULT_CHUNK_OVERLAP_TOKENS
|
||||
|
||||
|
||||
class OpenAICompatibleAdaptor(SkillAdaptor):
|
||||
"""
|
||||
Base class for OpenAI-compatible LLM platform adaptors.
|
||||
|
||||
Subclasses override these constants:
|
||||
- PLATFORM: Registry key (e.g., "kimi")
|
||||
- PLATFORM_NAME: Display name (e.g., "Kimi (Moonshot AI)")
|
||||
- DEFAULT_API_ENDPOINT: API base URL
|
||||
- DEFAULT_MODEL: Default model name
|
||||
- ENV_VAR_NAME: API key env var name
|
||||
- PLATFORM_URL: Dashboard/platform URL
|
||||
"""
|
||||
|
||||
PLATFORM = "unknown"
|
||||
PLATFORM_NAME = "Unknown"
|
||||
DEFAULT_API_ENDPOINT = ""
|
||||
DEFAULT_MODEL = ""
|
||||
ENV_VAR_NAME = ""
|
||||
PLATFORM_URL = ""
|
||||
|
||||
def format_skill_md(self, skill_dir: Path, metadata: SkillMetadata) -> str:
|
||||
"""
|
||||
Format SKILL.md as system instructions (no YAML frontmatter).
|
||||
|
||||
Uses plain text format compatible with OpenAI-compatible chat APIs.
|
||||
"""
|
||||
existing_content = self._read_existing_content(skill_dir)
|
||||
|
||||
if existing_content and len(existing_content) > 100:
|
||||
return f"""You are an expert assistant for {metadata.name}.
|
||||
|
||||
{metadata.description}
|
||||
|
||||
Use the attached knowledge files to provide accurate, detailed answers about {metadata.name}.
|
||||
|
||||
{existing_content}
|
||||
|
||||
## How to Assist Users
|
||||
|
||||
When users ask questions:
|
||||
1. Search the knowledge files for relevant information
|
||||
2. Provide clear, practical answers with code examples
|
||||
3. Reference specific documentation sections when helpful
|
||||
4. Be concise but thorough
|
||||
|
||||
Always prioritize accuracy by consulting the knowledge base before responding."""
|
||||
|
||||
return f"""You are an expert assistant for {metadata.name}.
|
||||
|
||||
{metadata.description}
|
||||
|
||||
## Your Knowledge Base
|
||||
|
||||
You have access to comprehensive documentation files about {metadata.name}. Use these files to provide accurate answers to user questions.
|
||||
|
||||
{self._generate_toc(skill_dir)}
|
||||
|
||||
## Quick Reference
|
||||
|
||||
{self._extract_quick_reference(skill_dir)}
|
||||
|
||||
## How to Assist Users
|
||||
|
||||
When users ask questions about {metadata.name}:
|
||||
|
||||
1. **Search the knowledge files** - Find relevant information in the documentation
|
||||
2. **Provide code examples** - Include practical, working code snippets
|
||||
3. **Reference documentation** - Cite specific sections when helpful
|
||||
4. **Be practical** - Focus on real-world usage and best practices
|
||||
5. **Stay accurate** - Always verify information against the knowledge base
|
||||
|
||||
## Response Guidelines
|
||||
|
||||
- Keep answers clear and concise
|
||||
- Use proper code formatting with language tags
|
||||
- Provide both simple and detailed explanations as needed
|
||||
- Suggest related topics when relevant
|
||||
- Admit when information isn't in the knowledge base
|
||||
|
||||
Always prioritize accuracy by consulting the attached documentation files before responding."""
|
||||
|
||||
def package(
|
||||
self,
|
||||
skill_dir: Path,
|
||||
output_path: Path,
|
||||
enable_chunking: bool = False,
|
||||
chunk_max_tokens: int = DEFAULT_CHUNK_TOKENS,
|
||||
preserve_code_blocks: bool = True,
|
||||
chunk_overlap_tokens: int = DEFAULT_CHUNK_OVERLAP_TOKENS,
|
||||
) -> Path:
|
||||
"""
|
||||
Package skill into ZIP file for the platform.
|
||||
|
||||
Creates platform-compatible structure:
|
||||
- system_instructions.txt (main instructions)
|
||||
- knowledge_files/*.md (reference files)
|
||||
- {platform}_metadata.json (skill metadata)
|
||||
"""
|
||||
skill_dir = Path(skill_dir)
|
||||
output_path = Path(output_path)
|
||||
|
||||
suffix = f"-{self.PLATFORM}.zip"
|
||||
|
||||
if output_path.is_dir() or str(output_path).endswith("/"):
|
||||
output_path = Path(output_path) / f"{skill_dir.name}{suffix}"
|
||||
elif not str(output_path).endswith(suffix):
|
||||
output_str = str(output_path)
|
||||
# Strip existing .zip extension if present
|
||||
if output_str.endswith(".zip"):
|
||||
output_str = output_str[:-4]
|
||||
output_path = Path(output_str + suffix)
|
||||
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||
skill_md = skill_dir / "SKILL.md"
|
||||
if skill_md.exists():
|
||||
instructions = skill_md.read_text(encoding="utf-8")
|
||||
zf.writestr("system_instructions.txt", instructions)
|
||||
|
||||
refs_dir = skill_dir / "references"
|
||||
if refs_dir.exists():
|
||||
for ref_file in refs_dir.rglob("*.md"):
|
||||
if ref_file.is_file() and not ref_file.name.startswith("."):
|
||||
arcname = f"knowledge_files/{ref_file.name}"
|
||||
zf.write(ref_file, arcname)
|
||||
|
||||
metadata = {
|
||||
"platform": self.PLATFORM,
|
||||
"name": skill_dir.name,
|
||||
"version": "1.0.0",
|
||||
"created_with": "skill-seekers",
|
||||
"model": self.DEFAULT_MODEL,
|
||||
"api_base": self.DEFAULT_API_ENDPOINT,
|
||||
}
|
||||
|
||||
zf.writestr(f"{self.PLATFORM}_metadata.json", json.dumps(metadata, indent=2))
|
||||
|
||||
return output_path
|
||||
|
||||
def upload(self, package_path: Path, api_key: str, **kwargs) -> dict[str, Any]:
|
||||
"""
|
||||
Upload/validate packaged skill via OpenAI-compatible API.
|
||||
"""
|
||||
package_path = Path(package_path)
|
||||
if not package_path.exists():
|
||||
return {
|
||||
"success": False,
|
||||
"skill_id": None,
|
||||
"url": None,
|
||||
"message": f"File not found: {package_path}",
|
||||
}
|
||||
|
||||
if package_path.suffix != ".zip":
|
||||
return {
|
||||
"success": False,
|
||||
"skill_id": None,
|
||||
"url": None,
|
||||
"message": f"Not a ZIP file: {package_path}",
|
||||
}
|
||||
|
||||
try:
|
||||
from openai import OpenAI, APITimeoutError, APIConnectionError
|
||||
except ImportError:
|
||||
return {
|
||||
"success": False,
|
||||
"skill_id": None,
|
||||
"url": None,
|
||||
"message": "openai library not installed. Run: pip install openai",
|
||||
}
|
||||
|
||||
try:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
with zipfile.ZipFile(package_path, "r") as zf:
|
||||
zf.extractall(temp_dir)
|
||||
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
instructions_file = temp_path / "system_instructions.txt"
|
||||
if not instructions_file.exists():
|
||||
return {
|
||||
"success": False,
|
||||
"skill_id": None,
|
||||
"url": None,
|
||||
"message": "Invalid package: system_instructions.txt not found",
|
||||
}
|
||||
|
||||
instructions = instructions_file.read_text(encoding="utf-8")
|
||||
|
||||
metadata_file = temp_path / f"{self.PLATFORM}_metadata.json"
|
||||
skill_name = package_path.stem
|
||||
model = kwargs.get("model", self.DEFAULT_MODEL)
|
||||
|
||||
if metadata_file.exists():
|
||||
with open(metadata_file) as f:
|
||||
metadata = json.load(f)
|
||||
skill_name = metadata.get("name", skill_name)
|
||||
model = metadata.get("model", model)
|
||||
|
||||
knowledge_dir = temp_path / "knowledge_files"
|
||||
knowledge_count = 0
|
||||
if knowledge_dir.exists():
|
||||
knowledge_count = len(list(knowledge_dir.glob("*.md")))
|
||||
|
||||
client = OpenAI(
|
||||
api_key=api_key,
|
||||
base_url=self.DEFAULT_API_ENDPOINT,
|
||||
)
|
||||
|
||||
client.chat.completions.create(
|
||||
model=model,
|
||||
messages=[
|
||||
{"role": "system", "content": instructions},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Confirm you are ready to assist with {skill_name}. Reply briefly.",
|
||||
},
|
||||
],
|
||||
temperature=0.3,
|
||||
max_tokens=100,
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"skill_id": None,
|
||||
"url": self.PLATFORM_URL,
|
||||
"message": f"Skill '{skill_name}' validated with {self.PLATFORM_NAME} {model} ({knowledge_count} knowledge files)",
|
||||
}
|
||||
|
||||
except APITimeoutError:
|
||||
return {
|
||||
"success": False,
|
||||
"skill_id": None,
|
||||
"url": None,
|
||||
"message": "Upload timed out. Try again.",
|
||||
}
|
||||
except APIConnectionError:
|
||||
return {
|
||||
"success": False,
|
||||
"skill_id": None,
|
||||
"url": None,
|
||||
"message": "Connection error. Check your internet connection.",
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"skill_id": None,
|
||||
"url": None,
|
||||
"message": f"Upload failed: {str(e)}",
|
||||
}
|
||||
|
||||
def validate_api_key(self, api_key: str) -> bool:
|
||||
"""Validate API key (non-empty, >10 chars)."""
|
||||
key = api_key.strip()
|
||||
return len(key) > 10
|
||||
|
||||
def get_env_var_name(self) -> str:
|
||||
"""Get environment variable name for API key."""
|
||||
return self.ENV_VAR_NAME
|
||||
|
||||
def supports_enhancement(self) -> bool:
|
||||
"""OpenAI-compatible platforms support enhancement."""
|
||||
return True
|
||||
|
||||
def enhance(self, skill_dir: Path, api_key: str) -> bool:
|
||||
"""
|
||||
Enhance SKILL.md using the platform's OpenAI-compatible API.
|
||||
"""
|
||||
try:
|
||||
from openai import OpenAI
|
||||
except ImportError:
|
||||
print("Error: openai package not installed")
|
||||
print("Install with: pip install openai")
|
||||
return False
|
||||
|
||||
skill_dir = Path(skill_dir)
|
||||
references_dir = skill_dir / "references"
|
||||
skill_md_path = skill_dir / "SKILL.md"
|
||||
|
||||
print("Reading reference documentation...")
|
||||
references = self._read_reference_files(references_dir)
|
||||
|
||||
if not references:
|
||||
print("No reference files found to analyze")
|
||||
return False
|
||||
|
||||
print(f" Read {len(references)} reference files")
|
||||
total_size = sum(len(c) for c in references.values())
|
||||
print(f" Total size: {total_size:,} characters\n")
|
||||
|
||||
current_skill_md = None
|
||||
if skill_md_path.exists():
|
||||
current_skill_md = skill_md_path.read_text(encoding="utf-8")
|
||||
print(f" Found existing SKILL.md ({len(current_skill_md)} chars)")
|
||||
else:
|
||||
print(" No existing SKILL.md, will create new one")
|
||||
|
||||
prompt = self._build_enhancement_prompt(skill_dir.name, references, current_skill_md)
|
||||
|
||||
print(f"\nAsking {self.PLATFORM_NAME} ({self.DEFAULT_MODEL}) to enhance SKILL.md...")
|
||||
print(f" Input: {len(prompt):,} characters")
|
||||
|
||||
try:
|
||||
client = OpenAI(
|
||||
api_key=api_key,
|
||||
base_url=self.DEFAULT_API_ENDPOINT,
|
||||
)
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model=self.DEFAULT_MODEL,
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": f"You are an expert technical writer creating system instructions for {self.PLATFORM_NAME}.",
|
||||
},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
temperature=0.3,
|
||||
max_tokens=4096,
|
||||
)
|
||||
|
||||
enhanced_content = response.choices[0].message.content
|
||||
print(f" Generated enhanced SKILL.md ({len(enhanced_content)} chars)\n")
|
||||
|
||||
if skill_md_path.exists():
|
||||
backup_path = skill_md_path.with_suffix(".md.backup")
|
||||
skill_md_path.rename(backup_path)
|
||||
print(f" Backed up original to: {backup_path.name}")
|
||||
|
||||
skill_md_path.write_text(enhanced_content, encoding="utf-8")
|
||||
print(" Saved enhanced SKILL.md")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error calling {self.PLATFORM_NAME} API: {e}")
|
||||
return False
|
||||
|
||||
def _read_reference_files(
|
||||
self, references_dir: Path, max_chars: int = 200000
|
||||
) -> dict[str, str]:
|
||||
"""Read reference markdown files from skill directory."""
|
||||
if not references_dir.exists():
|
||||
return {}
|
||||
|
||||
references = {}
|
||||
total_chars = 0
|
||||
|
||||
for ref_file in sorted(references_dir.glob("*.md")):
|
||||
if total_chars >= max_chars:
|
||||
break
|
||||
|
||||
try:
|
||||
content = ref_file.read_text(encoding="utf-8")
|
||||
if len(content) > 30000:
|
||||
content = content[:30000] + "\n\n...(truncated)"
|
||||
|
||||
references[ref_file.name] = content
|
||||
total_chars += len(content)
|
||||
|
||||
except Exception as e:
|
||||
print(f" Could not read {ref_file.name}: {e}")
|
||||
|
||||
return references
|
||||
|
||||
def _build_enhancement_prompt(
|
||||
self, skill_name: str, references: dict[str, str], current_skill_md: str = None
|
||||
) -> str:
|
||||
"""Build API prompt for enhancement."""
|
||||
prompt = f"""You are creating system instructions for a {self.PLATFORM_NAME} assistant about: {skill_name}
|
||||
|
||||
I've scraped documentation and organized it into reference files. Your job is to create EXCELLENT system instructions that will help the assistant use this documentation effectively.
|
||||
|
||||
CURRENT INSTRUCTIONS:
|
||||
{"```" if current_skill_md else "(none - create from scratch)"}
|
||||
{current_skill_md or "No existing instructions"}
|
||||
{"```" if current_skill_md else ""}
|
||||
|
||||
REFERENCE DOCUMENTATION:
|
||||
"""
|
||||
|
||||
for filename, content in references.items():
|
||||
prompt += f"\n\n## {filename}\n```markdown\n{content[:30000]}\n```\n"
|
||||
|
||||
prompt += f"""
|
||||
|
||||
YOUR TASK:
|
||||
Create enhanced system instructions that include:
|
||||
|
||||
1. **Clear role definition** - "You are an expert assistant for [topic]"
|
||||
2. **Knowledge base description** - What documentation is attached
|
||||
3. **Excellent Quick Reference** - Extract 5-10 of the BEST, most practical code examples from the reference docs
|
||||
- Choose SHORT, clear examples that demonstrate common tasks
|
||||
- Include both simple and intermediate examples
|
||||
- Annotate examples with clear descriptions
|
||||
- Use proper language tags (cpp, python, javascript, json, etc.)
|
||||
4. **Response guidelines** - How the assistant should help users
|
||||
5. **Search strategy** - How to find information in the knowledge base
|
||||
6. **DO NOT use YAML frontmatter** - This is plain text instructions
|
||||
|
||||
IMPORTANT:
|
||||
- Extract REAL examples from the reference docs, don't make them up
|
||||
- Prioritize SHORT, clear examples (5-20 lines max)
|
||||
- Make it actionable and practical
|
||||
- Write clear, direct instructions
|
||||
- Focus on how the assistant should behave and respond
|
||||
- NO YAML frontmatter (no --- blocks)
|
||||
|
||||
OUTPUT:
|
||||
Return ONLY the complete system instructions as plain text.
|
||||
"""
|
||||
|
||||
return prompt
|
||||
188
src/skill_seekers/cli/adaptors/opencode.py
Normal file
188
src/skill_seekers/cli/adaptors/opencode.py
Normal file
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
OpenCode Adaptor
|
||||
|
||||
Generates skills in OpenCode-compatible format with YAML frontmatter.
|
||||
OpenCode searches ~/.opencode/skills/ for SKILL.md files.
|
||||
"""
|
||||
|
||||
import re
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .base import SkillAdaptor, SkillMetadata
|
||||
from skill_seekers.cli.arguments.common import DEFAULT_CHUNK_TOKENS, DEFAULT_CHUNK_OVERLAP_TOKENS
|
||||
|
||||
|
||||
class OpenCodeAdaptor(SkillAdaptor):
|
||||
"""
|
||||
OpenCode platform adaptor.
|
||||
|
||||
Generates directory-based skill packages with dual-format YAML frontmatter
|
||||
compatible with both OpenCode and Claude Code.
|
||||
"""
|
||||
|
||||
PLATFORM = "opencode"
|
||||
PLATFORM_NAME = "OpenCode"
|
||||
DEFAULT_API_ENDPOINT = None # Local file-based, no API
|
||||
|
||||
# OpenCode name validation: kebab-case, 1-64 chars
|
||||
NAME_REGEX = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
|
||||
|
||||
@staticmethod
|
||||
def _to_kebab_case(name: str) -> str:
|
||||
"""
|
||||
Convert any title/name to valid OpenCode kebab-case.
|
||||
|
||||
Rules:
|
||||
- Lowercase
|
||||
- Replace spaces, underscores, dots with hyphens
|
||||
- Remove non-alphanumeric chars (except hyphens)
|
||||
- Collapse multiple hyphens
|
||||
- Strip leading/trailing hyphens
|
||||
- Truncate to 64 chars
|
||||
|
||||
Args:
|
||||
name: Input name string
|
||||
|
||||
Returns:
|
||||
Valid kebab-case name (1-64 chars)
|
||||
"""
|
||||
result = name.lower()
|
||||
result = re.sub(r"[_\s.]+", "-", result)
|
||||
result = re.sub(r"[^a-z0-9-]", "", result)
|
||||
result = re.sub(r"-+", "-", result)
|
||||
result = result.strip("-")
|
||||
result = result[:64]
|
||||
result = result.rstrip("-")
|
||||
return result or "skill"
|
||||
|
||||
def format_skill_md(self, skill_dir: Path, metadata: SkillMetadata) -> str:
|
||||
"""
|
||||
Format SKILL.md with OpenCode-compatible YAML frontmatter.
|
||||
|
||||
Generates a superset frontmatter that works with both Claude and OpenCode.
|
||||
OpenCode-required fields: kebab-case name, compatibility, metadata map.
|
||||
|
||||
Args:
|
||||
skill_dir: Path to skill directory
|
||||
metadata: Skill metadata
|
||||
|
||||
Returns:
|
||||
Formatted SKILL.md content with YAML frontmatter
|
||||
"""
|
||||
existing_content = self._read_existing_content(skill_dir)
|
||||
kebab_name = self._to_kebab_case(metadata.name)
|
||||
description = metadata.description[:1024] if metadata.description else ""
|
||||
|
||||
# Quote description to handle colons and special YAML chars
|
||||
safe_desc = description.replace('"', '\\"')
|
||||
safe_source = metadata.name.replace('"', '\\"')
|
||||
|
||||
frontmatter = f"""---
|
||||
name: {kebab_name}
|
||||
description: "{safe_desc}"
|
||||
version: {metadata.version}
|
||||
license: MIT
|
||||
compatibility: opencode
|
||||
metadata:
|
||||
generated-by: skill-seekers
|
||||
source: "{safe_source}"
|
||||
version: {metadata.version}
|
||||
---"""
|
||||
|
||||
if existing_content and len(existing_content) > 100:
|
||||
return f"{frontmatter}\n\n{existing_content}"
|
||||
|
||||
toc = self._generate_toc(skill_dir)
|
||||
quick_ref = self._extract_quick_reference(skill_dir)
|
||||
|
||||
body = f"""# {metadata.name}
|
||||
|
||||
{metadata.description}
|
||||
|
||||
## Documentation
|
||||
|
||||
{toc if toc else "See references/ directory for documentation."}
|
||||
|
||||
## Quick Reference
|
||||
|
||||
{quick_ref}"""
|
||||
|
||||
return f"{frontmatter}\n\n{body}"
|
||||
|
||||
def package(
|
||||
self,
|
||||
skill_dir: Path,
|
||||
output_path: Path,
|
||||
enable_chunking: bool = False,
|
||||
chunk_max_tokens: int = DEFAULT_CHUNK_TOKENS,
|
||||
preserve_code_blocks: bool = True,
|
||||
chunk_overlap_tokens: int = DEFAULT_CHUNK_OVERLAP_TOKENS,
|
||||
) -> Path:
|
||||
"""
|
||||
Package skill as a directory (not ZIP) for OpenCode.
|
||||
|
||||
Creates: <output>/<name>-opencode/SKILL.md + references/
|
||||
|
||||
Args:
|
||||
skill_dir: Path to skill directory
|
||||
output_path: Output path for the package directory
|
||||
|
||||
Returns:
|
||||
Path to created directory
|
||||
"""
|
||||
skill_dir = Path(skill_dir)
|
||||
output_path = Path(output_path)
|
||||
|
||||
dir_name = f"{skill_dir.name}-opencode"
|
||||
|
||||
if output_path.is_dir() or str(output_path).endswith("/"):
|
||||
target_dir = output_path / dir_name
|
||||
else:
|
||||
target_dir = output_path
|
||||
|
||||
# Clean and create target
|
||||
if target_dir.exists():
|
||||
shutil.rmtree(target_dir)
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Copy SKILL.md
|
||||
skill_md = skill_dir / "SKILL.md"
|
||||
if skill_md.exists():
|
||||
shutil.copy2(skill_md, target_dir / "SKILL.md")
|
||||
|
||||
# Copy references
|
||||
refs_dir = skill_dir / "references"
|
||||
if refs_dir.exists():
|
||||
target_refs = target_dir / "references"
|
||||
shutil.copytree(
|
||||
refs_dir,
|
||||
target_refs,
|
||||
ignore=shutil.ignore_patterns("*.backup", ".*"),
|
||||
)
|
||||
|
||||
return target_dir
|
||||
|
||||
def upload(self, package_path: Path, api_key: str, **kwargs) -> dict[str, Any]:
|
||||
"""
|
||||
OpenCode uses local files, no upload needed.
|
||||
|
||||
Returns local path information.
|
||||
"""
|
||||
package_path = Path(package_path)
|
||||
return {
|
||||
"success": True,
|
||||
"skill_id": None,
|
||||
"url": None,
|
||||
"message": f"OpenCode skill packaged at: {package_path} (local install only)",
|
||||
}
|
||||
|
||||
def validate_api_key(self, api_key: str) -> bool:
|
||||
"""No API key needed for OpenCode."""
|
||||
return True
|
||||
|
||||
def supports_enhancement(self) -> bool:
|
||||
"""OpenCode does not have its own enhancement API."""
|
||||
return False
|
||||
19
src/skill_seekers/cli/adaptors/openrouter.py
Normal file
19
src/skill_seekers/cli/adaptors/openrouter.py
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
OpenRouter Adaptor
|
||||
|
||||
OpenAI-compatible LLM platform adaptor for OpenRouter.
|
||||
"""
|
||||
|
||||
from .openai_compatible import OpenAICompatibleAdaptor
|
||||
|
||||
|
||||
class OpenRouterAdaptor(OpenAICompatibleAdaptor):
|
||||
"""OpenRouter platform adaptor."""
|
||||
|
||||
PLATFORM = "openrouter"
|
||||
PLATFORM_NAME = "OpenRouter"
|
||||
DEFAULT_API_ENDPOINT = "https://openrouter.ai/api/v1"
|
||||
DEFAULT_MODEL = "openrouter/auto"
|
||||
ENV_VAR_NAME = "OPENROUTER_API_KEY"
|
||||
PLATFORM_URL = "https://openrouter.ai/"
|
||||
19
src/skill_seekers/cli/adaptors/qwen.py
Normal file
19
src/skill_seekers/cli/adaptors/qwen.py
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Qwen (Alibaba) Adaptor
|
||||
|
||||
OpenAI-compatible LLM platform adaptor for Qwen/DashScope.
|
||||
"""
|
||||
|
||||
from .openai_compatible import OpenAICompatibleAdaptor
|
||||
|
||||
|
||||
class QwenAdaptor(OpenAICompatibleAdaptor):
|
||||
"""Qwen (Alibaba Cloud) platform adaptor."""
|
||||
|
||||
PLATFORM = "qwen"
|
||||
PLATFORM_NAME = "Qwen (Alibaba)"
|
||||
DEFAULT_API_ENDPOINT = "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||
DEFAULT_MODEL = "qwen-max"
|
||||
ENV_VAR_NAME = "DASHSCOPE_API_KEY"
|
||||
PLATFORM_URL = "https://dashscope.console.aliyun.com/"
|
||||
19
src/skill_seekers/cli/adaptors/together.py
Normal file
19
src/skill_seekers/cli/adaptors/together.py
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Together AI Adaptor
|
||||
|
||||
OpenAI-compatible LLM platform adaptor for Together AI.
|
||||
"""
|
||||
|
||||
from .openai_compatible import OpenAICompatibleAdaptor
|
||||
|
||||
|
||||
class TogetherAdaptor(OpenAICompatibleAdaptor):
|
||||
"""Together AI platform adaptor."""
|
||||
|
||||
PLATFORM = "together"
|
||||
PLATFORM_NAME = "Together AI"
|
||||
DEFAULT_API_ENDPOINT = "https://api.together.xyz/v1"
|
||||
DEFAULT_MODEL = "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo"
|
||||
ENV_VAR_NAME = "TOGETHER_API_KEY"
|
||||
PLATFORM_URL = "https://api.together.xyz/"
|
||||
@@ -44,6 +44,13 @@ AGENT_PATHS = {
|
||||
"aide": "~/.aide/skills/", # Global
|
||||
"windsurf": "~/.windsurf/skills/", # Global
|
||||
"neovate": "~/.neovate/skills/", # Global
|
||||
"roo": ".roo/skills/", # Project-relative (Roo Code, Cline fork)
|
||||
"cline": ".cline/skills/", # Project-relative (Cline AI)
|
||||
"aider": "~/.aider/skills/", # Global (terminal AI coding)
|
||||
"bolt": ".bolt/skills/", # Project-relative (Bolt.new/Bolt.diy)
|
||||
"kilo": ".kilo/skills/", # Project-relative (Kilo Code, Cline fork)
|
||||
"continue": "~/.continue/skills/", # Global (Continue.dev)
|
||||
"kimi-code": "~/.kimi/skills/", # Global (Kimi Code)
|
||||
}
|
||||
|
||||
|
||||
@@ -360,7 +367,8 @@ Examples:
|
||||
skill-seekers install-agent output/react/ --agent cursor --dry-run
|
||||
|
||||
Supported agents:
|
||||
claude, cursor, vscode, copilot, amp, goose, opencode, letta, aide, windsurf, neovate, all
|
||||
claude, cursor, vscode, copilot, amp, goose, opencode, letta, aide, windsurf,
|
||||
neovate, roo, cline, aider, bolt, kilo, continue, kimi-code, all
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
447
src/skill_seekers/cli/opencode_skill_splitter.py
Normal file
447
src/skill_seekers/cli/opencode_skill_splitter.py
Normal file
@@ -0,0 +1,447 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
OpenCode Skill Splitter
|
||||
|
||||
Splits large documentation skills into multiple focused sub-skills for
|
||||
OpenCode's on-demand loading. Reuses existing split_config + generate_router patterns.
|
||||
|
||||
Usage:
|
||||
skill-seekers opencode-split <skill_directory> [--max-size 50000] [--output-dir output/]
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from skill_seekers.cli.adaptors.opencode import OpenCodeAdaptor
|
||||
|
||||
|
||||
class OpenCodeSkillSplitter:
|
||||
"""
|
||||
Splits large skills into multiple focused sub-skills for OpenCode.
|
||||
|
||||
Strategy:
|
||||
1. Read SKILL.md and references
|
||||
2. Split by H2 sections in SKILL.md (or by reference files if no sections)
|
||||
3. Generate a router SKILL.md that lists all sub-skills
|
||||
4. Output each sub-skill with OpenCode-compatible frontmatter
|
||||
"""
|
||||
|
||||
def __init__(self, skill_dir: str | Path, max_chars: int = 50000):
|
||||
self.skill_dir = Path(skill_dir)
|
||||
self.max_chars = max_chars
|
||||
self.adaptor = OpenCodeAdaptor()
|
||||
|
||||
def needs_splitting(self) -> bool:
|
||||
"""Check if the skill exceeds the size threshold."""
|
||||
total = 0
|
||||
skill_md = self.skill_dir / "SKILL.md"
|
||||
if skill_md.exists():
|
||||
total += skill_md.stat().st_size
|
||||
|
||||
refs_dir = self.skill_dir / "references"
|
||||
if refs_dir.exists():
|
||||
for f in refs_dir.rglob("*.md"):
|
||||
total += f.stat().st_size
|
||||
|
||||
return total > self.max_chars
|
||||
|
||||
def _extract_sections(self, content: str) -> list[dict[str, str]]:
|
||||
"""
|
||||
Extract H2 sections from markdown content.
|
||||
|
||||
Returns list of {title, content} dicts.
|
||||
"""
|
||||
# Strip YAML frontmatter
|
||||
if content.startswith("---"):
|
||||
parts = content.split("---", 2)
|
||||
if len(parts) >= 3:
|
||||
content = parts[2]
|
||||
|
||||
sections = []
|
||||
# Split on ## headers
|
||||
pattern = re.compile(r"^## (.+)$", re.MULTILINE)
|
||||
matches = list(pattern.finditer(content))
|
||||
|
||||
if not matches:
|
||||
return [{"title": "main", "content": content.strip()}]
|
||||
|
||||
# Content before first section
|
||||
preamble = content[: matches[0].start()].strip()
|
||||
if preamble:
|
||||
sections.append({"title": "overview", "content": preamble})
|
||||
|
||||
for i, match in enumerate(matches):
|
||||
title = match.group(1).strip()
|
||||
start = match.end()
|
||||
end = matches[i + 1].start() if i + 1 < len(matches) else len(content)
|
||||
section_content = content[start:end].strip()
|
||||
if section_content:
|
||||
sections.append({"title": title, "content": f"## {title}\n\n{section_content}"})
|
||||
|
||||
return sections
|
||||
|
||||
def _group_small_sections(self, sections: list[dict[str, str]]) -> list[dict[str, str]]:
|
||||
"""Merge sections that are too small to be standalone skills."""
|
||||
if not sections:
|
||||
return sections
|
||||
|
||||
grouped = []
|
||||
current = None
|
||||
|
||||
for section in sections:
|
||||
if current is None:
|
||||
current = dict(section)
|
||||
continue
|
||||
|
||||
combined_size = len(current["content"]) + len(section["content"])
|
||||
if combined_size < self.max_chars // 4:
|
||||
# Merge small sections
|
||||
current["title"] = f"{current['title']}-and-{section['title']}"
|
||||
current["content"] += f"\n\n{section['content']}"
|
||||
else:
|
||||
grouped.append(current)
|
||||
current = dict(section)
|
||||
|
||||
if current:
|
||||
grouped.append(current)
|
||||
|
||||
return grouped
|
||||
|
||||
def split(self, output_dir: str | Path | None = None) -> list[Path]:
|
||||
"""
|
||||
Split the skill into multiple sub-skills.
|
||||
|
||||
Args:
|
||||
output_dir: Output directory (default: <skill_dir>-split/)
|
||||
|
||||
Returns:
|
||||
List of paths to created sub-skill directories
|
||||
"""
|
||||
if output_dir is None:
|
||||
output_dir = self.skill_dir.parent / f"{self.skill_dir.name}-opencode-split"
|
||||
output_dir = Path(output_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
skill_name = self.skill_dir.name
|
||||
base_name = OpenCodeAdaptor._to_kebab_case(skill_name)
|
||||
|
||||
# Read SKILL.md
|
||||
skill_md = self.skill_dir / "SKILL.md"
|
||||
if not skill_md.exists():
|
||||
print(f"Error: SKILL.md not found in {self.skill_dir}")
|
||||
return []
|
||||
|
||||
content = skill_md.read_text(encoding="utf-8")
|
||||
|
||||
# Extract and group sections
|
||||
sections = self._extract_sections(content)
|
||||
sections = self._group_small_sections(sections)
|
||||
|
||||
if len(sections) <= 1:
|
||||
# Try splitting by reference files instead
|
||||
sections = self._split_by_references()
|
||||
|
||||
if len(sections) <= 1:
|
||||
print(f"Skill {skill_name} has only 1 section, no splitting needed")
|
||||
return [self.skill_dir]
|
||||
|
||||
created_dirs = []
|
||||
sub_skill_names = []
|
||||
|
||||
# Create sub-skills
|
||||
for section in sections:
|
||||
section_name = OpenCodeAdaptor._to_kebab_case(section["title"])
|
||||
sub_name = f"{base_name}-{section_name}"
|
||||
sub_dir = output_dir / sub_name
|
||||
sub_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Write sub-skill SKILL.md with frontmatter (quote values for YAML safety)
|
||||
safe_title = section["title"].replace('"', '\\"')
|
||||
safe_skill = skill_name.replace('"', '\\"')
|
||||
frontmatter = f"""---
|
||||
name: {sub_name}
|
||||
description: "{safe_skill} - {safe_title}"
|
||||
version: 1.0.0
|
||||
license: MIT
|
||||
compatibility: opencode
|
||||
metadata:
|
||||
generated-by: skill-seekers
|
||||
source: "{safe_skill}"
|
||||
parent-skill: {base_name}
|
||||
section: "{safe_title}"
|
||||
---"""
|
||||
|
||||
sub_content = (
|
||||
f"{frontmatter}\n\n# {skill_name} - {section['title']}\n\n{section['content']}"
|
||||
)
|
||||
(sub_dir / "SKILL.md").write_text(sub_content, encoding="utf-8")
|
||||
|
||||
sub_skill_names.append(sub_name)
|
||||
created_dirs.append(sub_dir)
|
||||
|
||||
# Create router skill
|
||||
router_dir = output_dir / base_name
|
||||
router_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
router_content = self._generate_router(base_name, skill_name, sub_skill_names)
|
||||
(router_dir / "SKILL.md").write_text(router_content, encoding="utf-8")
|
||||
created_dirs.insert(0, router_dir)
|
||||
|
||||
print(f"Split '{skill_name}' into {len(sub_skill_names)} sub-skills + 1 router:")
|
||||
print(f" Router: {base_name}/")
|
||||
for name in sub_skill_names:
|
||||
print(f" Sub-skill: {name}/")
|
||||
|
||||
return created_dirs
|
||||
|
||||
def _split_by_references(self) -> list[dict[str, str]]:
|
||||
"""Split by reference files when SKILL.md doesn't have enough sections."""
|
||||
refs_dir = self.skill_dir / "references"
|
||||
if not refs_dir.exists():
|
||||
return []
|
||||
|
||||
sections = []
|
||||
for ref_file in sorted(refs_dir.glob("*.md")):
|
||||
if ref_file.name.startswith(".") or ref_file.name == "index.md":
|
||||
continue
|
||||
try:
|
||||
content = ref_file.read_text(encoding="utf-8")
|
||||
title = ref_file.stem.replace("_", " ").replace("-", " ")
|
||||
sections.append({"title": title, "content": content})
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return sections
|
||||
|
||||
def _generate_router(self, base_name: str, skill_name: str, sub_skill_names: list[str]) -> str:
|
||||
"""Generate a router SKILL.md that lists all sub-skills."""
|
||||
safe_skill = skill_name.replace('"', '\\"')
|
||||
frontmatter = f"""---
|
||||
name: {base_name}
|
||||
description: "Router for {safe_skill} documentation. Directs to specialized sub-skills."
|
||||
version: 1.0.0
|
||||
license: MIT
|
||||
compatibility: opencode
|
||||
metadata:
|
||||
generated-by: skill-seekers
|
||||
source: "{safe_skill}"
|
||||
is-router: true
|
||||
sub-skills: {len(sub_skill_names)}
|
||||
---"""
|
||||
|
||||
sub_list = "\n".join(
|
||||
f"- `{name}` - {name.replace(base_name + '-', '').replace('-', ' ').title()}"
|
||||
for name in sub_skill_names
|
||||
)
|
||||
|
||||
body = f"""# {skill_name}
|
||||
|
||||
This is a router skill that directs to specialized sub-skills.
|
||||
|
||||
## Available Sub-Skills
|
||||
|
||||
{sub_list}
|
||||
|
||||
## Usage
|
||||
|
||||
When answering questions about {skill_name}, load the relevant sub-skill for detailed information.
|
||||
Each sub-skill covers a specific topic area of the documentation."""
|
||||
|
||||
return f"{frontmatter}\n\n{body}"
|
||||
|
||||
|
||||
class OpenCodeSkillConverter:
|
||||
"""
|
||||
Bi-directional skill format converter.
|
||||
|
||||
Converts between Skill Seekers format and OpenCode ecosystem format.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def import_opencode_skill(source_dir: str | Path) -> dict[str, Any]:
|
||||
"""
|
||||
Import a skill from OpenCode format into Skill Seekers format.
|
||||
|
||||
Reads an OpenCode skill directory and returns a normalized dict
|
||||
suitable for further processing by Skill Seekers adaptors.
|
||||
|
||||
Args:
|
||||
source_dir: Path to OpenCode skill directory
|
||||
|
||||
Returns:
|
||||
Dict with keys: name, description, version, content, references, metadata
|
||||
"""
|
||||
source_dir = Path(source_dir)
|
||||
|
||||
skill_md = source_dir / "SKILL.md"
|
||||
if not skill_md.exists():
|
||||
raise FileNotFoundError(f"SKILL.md not found in {source_dir}")
|
||||
|
||||
raw = skill_md.read_text(encoding="utf-8")
|
||||
|
||||
# Parse frontmatter
|
||||
frontmatter = {}
|
||||
content = raw
|
||||
if raw.startswith("---"):
|
||||
parts = raw.split("---", 2)
|
||||
if len(parts) >= 3:
|
||||
for line in parts[1].strip().splitlines():
|
||||
if ":" in line:
|
||||
key, _, value = line.partition(":")
|
||||
frontmatter[key.strip()] = value.strip()
|
||||
content = parts[2].strip()
|
||||
|
||||
# Read references
|
||||
references = {}
|
||||
refs_dir = source_dir / "references"
|
||||
if refs_dir.exists():
|
||||
for ref_file in sorted(refs_dir.glob("*.md")):
|
||||
if not ref_file.name.startswith("."):
|
||||
with contextlib.suppress(Exception):
|
||||
references[ref_file.name] = ref_file.read_text(encoding="utf-8")
|
||||
|
||||
return {
|
||||
"name": frontmatter.get("name", source_dir.name),
|
||||
"description": frontmatter.get("description", ""),
|
||||
"version": frontmatter.get("version", "1.0.0"),
|
||||
"content": content,
|
||||
"references": references,
|
||||
"metadata": frontmatter,
|
||||
"source_format": "opencode",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def export_to_target(
|
||||
skill_data: dict[str, Any],
|
||||
target: str,
|
||||
output_dir: str | Path,
|
||||
) -> Path:
|
||||
"""
|
||||
Export an imported skill to a target platform format.
|
||||
|
||||
Args:
|
||||
skill_data: Normalized skill dict from import_opencode_skill()
|
||||
target: Target platform ('claude', 'gemini', 'openai', 'markdown', etc.)
|
||||
output_dir: Output directory
|
||||
|
||||
Returns:
|
||||
Path to the exported skill directory
|
||||
"""
|
||||
from skill_seekers.cli.adaptors import get_adaptor
|
||||
from skill_seekers.cli.adaptors.base import SkillMetadata
|
||||
|
||||
output_dir = Path(output_dir)
|
||||
skill_dir = output_dir / skill_data["name"]
|
||||
skill_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Write SKILL.md (raw content without frontmatter for now)
|
||||
(skill_dir / "SKILL.md").write_text(skill_data["content"], encoding="utf-8")
|
||||
|
||||
# Write references
|
||||
if skill_data.get("references"):
|
||||
refs_dir = skill_dir / "references"
|
||||
refs_dir.mkdir(exist_ok=True)
|
||||
for name, content in skill_data["references"].items():
|
||||
(refs_dir / name).write_text(content, encoding="utf-8")
|
||||
|
||||
# Format using target adaptor
|
||||
adaptor = get_adaptor(target)
|
||||
metadata = SkillMetadata(
|
||||
name=skill_data["name"],
|
||||
description=skill_data.get("description", ""),
|
||||
version=skill_data.get("version", "1.0.0"),
|
||||
)
|
||||
|
||||
formatted = adaptor.format_skill_md(skill_dir, metadata)
|
||||
(skill_dir / "SKILL.md").write_text(formatted, encoding="utf-8")
|
||||
|
||||
return skill_dir
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="skill-seekers-opencode-split",
|
||||
description="Split large skills into OpenCode-compatible sub-skills",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
# Auto-split a large skill
|
||||
skill-seekers opencode-split output/react/
|
||||
|
||||
# Custom size threshold
|
||||
skill-seekers opencode-split output/react/ --max-size 30000
|
||||
|
||||
# Custom output directory
|
||||
skill-seekers opencode-split output/react/ --output-dir output/react-split/
|
||||
|
||||
# Import an OpenCode skill and convert to Claude format
|
||||
skill-seekers opencode-convert ~/.opencode/skills/my-skill/ --target claude --output-dir output/
|
||||
|
||||
# Check if splitting is needed
|
||||
skill-seekers opencode-split output/react/ --dry-run
|
||||
""",
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(dest="command")
|
||||
|
||||
# Split command
|
||||
split_parser = subparsers.add_parser("split", help="Split large skill into sub-skills")
|
||||
split_parser.add_argument("skill_directory", help="Path to skill directory")
|
||||
split_parser.add_argument(
|
||||
"--max-size", type=int, default=50000, help="Max chars before splitting (default: 50000)"
|
||||
)
|
||||
split_parser.add_argument("--output-dir", help="Output directory")
|
||||
split_parser.add_argument(
|
||||
"--dry-run", action="store_true", help="Check if splitting is needed without making changes"
|
||||
)
|
||||
|
||||
# Convert command
|
||||
convert_parser = subparsers.add_parser("convert", help="Convert between skill formats")
|
||||
convert_parser.add_argument("source_directory", help="Path to source skill directory")
|
||||
convert_parser.add_argument(
|
||||
"--target", required=True, help="Target platform (claude, gemini, openai, markdown, etc.)"
|
||||
)
|
||||
convert_parser.add_argument("--output-dir", required=True, help="Output directory")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == "split" or (not hasattr(args, "command") or args.command is None):
|
||||
# Default to split if no subcommand but has positional arg
|
||||
if not hasattr(args, "skill_directory"):
|
||||
parser.print_help()
|
||||
return 1
|
||||
|
||||
splitter = OpenCodeSkillSplitter(args.skill_directory, args.max_size)
|
||||
|
||||
if args.dry_run:
|
||||
if splitter.needs_splitting():
|
||||
print(f"Skill needs splitting (exceeds {args.max_size} chars)")
|
||||
else:
|
||||
print(f"Skill does not need splitting (under {args.max_size} chars)")
|
||||
return 0
|
||||
|
||||
result = splitter.split(args.output_dir)
|
||||
return 0 if result else 1
|
||||
|
||||
elif args.command == "convert":
|
||||
try:
|
||||
skill_data = OpenCodeSkillConverter.import_opencode_skill(args.source_directory)
|
||||
result = OpenCodeSkillConverter.export_to_target(
|
||||
skill_data, args.target, args.output_dir
|
||||
)
|
||||
print(f"Converted skill to {args.target} format: {result}")
|
||||
return 0
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return 1
|
||||
|
||||
parser.print_help()
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user