feat: Phase 4 - Implement MarkdownAdaptor for generic export

- Add MarkdownAdaptor for universal markdown export
- Pure markdown format (no platform-specific features)
- ZIP packaging with README.md, references/, DOCUMENTATION.md
- No upload capability (manual use only)
- No AI enhancement support
- Combines all references into single DOCUMENTATION.md
- Add 12 unit tests (all passing)

Test Results:
- 12 MarkdownAdaptor tests passing
- 45 total adaptor tests passing (4 skipped)

Phase 4 Complete 

Related to #179
This commit is contained in:
yusyus
2025-12-28 20:34:21 +03:00
parent 9032232ac7
commit 1a2f268316
2 changed files with 496 additions and 0 deletions

View File

@@ -0,0 +1,268 @@
#!/usr/bin/env python3
"""
Generic Markdown Adaptor
Implements generic markdown export for universal LLM compatibility.
No platform-specific features, just clean markdown documentation.
"""
import zipfile
from pathlib import Path
from typing import Dict, Any
from .base import SkillAdaptor, SkillMetadata
class MarkdownAdaptor(SkillAdaptor):
"""
Generic Markdown platform adaptor.
Handles:
- Pure markdown format (no platform-specific formatting)
- ZIP packaging with combined or individual files
- No upload capability (manual use)
- No AI enhancement (generic export only)
"""
PLATFORM = "markdown"
PLATFORM_NAME = "Generic Markdown (Universal)"
DEFAULT_API_ENDPOINT = None # No upload endpoint
def format_skill_md(self, skill_dir: Path, metadata: SkillMetadata) -> str:
"""
Format SKILL.md as pure markdown.
Clean, universal markdown that works with any LLM or documentation system.
Args:
skill_dir: Path to skill directory
metadata: Skill metadata
Returns:
Formatted markdown content
"""
# Read existing content (if any)
existing_content = self._read_existing_content(skill_dir)
# If existing content is substantial, use it
if existing_content and len(existing_content) > 100:
content_body = existing_content
else:
# Generate clean markdown
content_body = f"""# {metadata.name.title()} Documentation
{metadata.description}
## Table of Contents
{self._generate_toc(skill_dir)}
## Quick Reference
{self._extract_quick_reference(skill_dir)}
## Documentation
This documentation package contains comprehensive reference materials organized into categorized sections.
### Available Sections
{self._generate_toc(skill_dir)}
## Usage
Browse the reference files for detailed information on each topic. All files are in standard markdown format and can be viewed with any markdown reader or text editor.
---
*Documentation generated by Skill Seekers*
"""
# Return pure markdown (no frontmatter, no special formatting)
return content_body
def package(self, skill_dir: Path, output_path: Path) -> Path:
"""
Package skill into ZIP file with markdown documentation.
Creates universal structure:
- README.md (combined documentation)
- references/*.md (individual reference files)
- metadata.json (skill information)
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)
# Determine output filename
if output_path.is_dir() or str(output_path).endswith('/'):
output_path = Path(output_path) / f"{skill_dir.name}-markdown.zip"
elif not str(output_path).endswith('.zip'):
# Replace extension if needed
output_str = str(output_path).replace('.tar.gz', '.zip')
if not output_str.endswith('-markdown.zip'):
output_str = output_str.replace('.zip', '-markdown.zip')
if not output_str.endswith('.zip'):
output_str += '.zip'
output_path = Path(output_str)
output_path = Path(output_path)
output_path.parent.mkdir(parents=True, exist_ok=True)
# Create ZIP file
with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zf:
# Add SKILL.md as README.md
skill_md = skill_dir / "SKILL.md"
if skill_md.exists():
content = skill_md.read_text(encoding='utf-8')
zf.writestr("README.md", content)
# Add individual reference files
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('.'):
# Preserve directory structure under references/
arcname = ref_file.relative_to(skill_dir)
zf.write(ref_file, str(arcname))
# Create combined documentation file
combined = self._create_combined_doc(skill_dir)
if combined:
zf.writestr("DOCUMENTATION.md", combined)
# Add metadata file
import json
metadata = {
'platform': 'markdown',
'name': skill_dir.name,
'version': '1.0.0',
'created_with': 'skill-seekers',
'format': 'universal_markdown',
'usage': 'Use with any LLM or documentation system'
}
zf.writestr("metadata.json", json.dumps(metadata, indent=2))
return output_path
def upload(self, package_path: Path, api_key: str, **kwargs) -> Dict[str, Any]:
"""
Generic markdown export does not support upload.
Users should manually use the exported markdown files.
Args:
package_path: Path to package file
api_key: Not used
**kwargs: Not used
Returns:
Result indicating no upload capability
"""
return {
'success': False,
'skill_id': None,
'url': str(package_path.absolute()),
'message': (
'Generic markdown export does not support automatic upload. '
f'Your documentation is packaged at: {package_path.absolute()}'
)
}
def validate_api_key(self, api_key: str) -> bool:
"""
Markdown export doesn't use API keys.
Args:
api_key: Not used
Returns:
Always False (no API needed)
"""
return False
def get_env_var_name(self) -> str:
"""
No API key needed for markdown export.
Returns:
Empty string
"""
return ""
def supports_enhancement(self) -> bool:
"""
Markdown export doesn't support AI enhancement.
Returns:
False
"""
return False
def enhance(self, skill_dir: Path, api_key: str) -> bool:
"""
Markdown export doesn't support enhancement.
Args:
skill_dir: Not used
api_key: Not used
Returns:
False
"""
print("❌ Generic markdown export does not support AI enhancement")
print(" Use --target claude, --target gemini, or --target openai for enhancement")
return False
def _create_combined_doc(self, skill_dir: Path) -> str:
"""
Create a combined documentation file from all references.
Args:
skill_dir: Path to skill directory
Returns:
Combined markdown content
"""
skill_md = skill_dir / "SKILL.md"
refs_dir = skill_dir / "references"
combined_parts = []
# Add main content
if skill_md.exists():
content = skill_md.read_text(encoding='utf-8')
# Strip YAML frontmatter if present
if content.startswith('---'):
parts = content.split('---', 2)
if len(parts) >= 3:
content = parts[2].strip()
combined_parts.append(content)
# Add separator
combined_parts.append("\n\n---\n\n")
# Add all reference files
if refs_dir.exists():
# Sort for consistent ordering
ref_files = sorted(refs_dir.glob("*.md"))
for ref_file in ref_files:
if ref_file.name == "index.md":
continue # Skip index
try:
ref_content = ref_file.read_text(encoding='utf-8')
combined_parts.append(f"# {ref_file.stem.replace('_', ' ').title()}\n\n")
combined_parts.append(ref_content)
combined_parts.append("\n\n---\n\n")
except Exception:
pass # Skip files that can't be read
return "".join(combined_parts).strip()