diff --git a/README.md b/README.md index 708f253..52176af 100644 --- a/README.md +++ b/README.md @@ -303,6 +303,39 @@ skill-seekers install --config react --- +## šŸ“Š Feature Matrix + +Skill Seekers supports **4 platforms** and **5 skill modes** with full feature parity. + +**Platforms:** Claude AI, Google Gemini, OpenAI ChatGPT, Generic Markdown +**Skill Modes:** Documentation, GitHub, PDF, Unified Multi-Source, Local Repository + +See [Complete Feature Matrix](docs/FEATURE_MATRIX.md) for detailed platform and feature support. + +### Quick Platform Comparison + +| Feature | Claude | Gemini | OpenAI | Markdown | +|---------|--------|--------|--------|----------| +| Format | ZIP + YAML | tar.gz | ZIP + Vector | ZIP | +| Upload | āœ… API | āœ… API | āœ… API | āŒ Manual | +| Enhancement | āœ… Sonnet 4 | āœ… 2.0 Flash | āœ… GPT-4o | āŒ None | +| All Skill Modes | āœ… | āœ… | āœ… | āœ… | + +**Examples:** +```bash +# Package for all platforms (same skill) +skill-seekers package output/react/ --target claude +skill-seekers package output/react/ --target gemini +skill-seekers package output/react/ --target openai +skill-seekers package output/react/ --target markdown + +# Install for specific platform +skill-seekers install --config django --target gemini +skill-seekers install --config fastapi --target openai +``` + +--- + ## Usage Examples ### Documentation Scraping diff --git a/docs/FEATURE_MATRIX.md b/docs/FEATURE_MATRIX.md new file mode 100644 index 0000000..d2e49fc --- /dev/null +++ b/docs/FEATURE_MATRIX.md @@ -0,0 +1,321 @@ +# Skill Seekers Feature Matrix + +Complete feature support across all platforms and skill modes. + +## Platform Support + +| Platform | Package Format | Upload | Enhancement | API Key Required | +|----------|---------------|--------|-------------|------------------| +| **Claude AI** | ZIP | āœ… Anthropic API | āœ… Sonnet 4 | ANTHROPIC_API_KEY | +| **Google Gemini** | tar.gz | āœ… Files API | āœ… Gemini 2.0 | GOOGLE_API_KEY | +| **OpenAI ChatGPT** | ZIP | āœ… Assistants API | āœ… GPT-4o | OPENAI_API_KEY | +| **Generic Markdown** | ZIP | āŒ Manual | āŒ None | None | + +## Skill Mode Support + +| Mode | Description | Platforms | Example Configs | +|------|-------------|-----------|-----------------| +| **Documentation** | Scrape HTML docs | All 4 | react.json, django.json (14 total) | +| **GitHub** | Analyze repositories | All 4 | react_github.json, godot_github.json | +| **PDF** | Extract from PDFs | All 4 | example_pdf.json | +| **Unified** | Multi-source (docs+GitHub+PDF) | All 4 | react_unified.json (5 total) | +| **Local Repo** | Unlimited local analysis | All 4 | deck_deck_go_local.json | + +## CLI Command Support + +| Command | Platforms | Skill Modes | Multi-Platform Flag | +|---------|-----------|-------------|---------------------| +| `scrape` | All | Docs only | No (output is universal) | +| `github` | All | GitHub only | No (output is universal) | +| `pdf` | All | PDF only | No (output is universal) | +| `unified` | All | Unified only | No (output is universal) | +| `enhance` | Claude, Gemini, OpenAI | All | āœ… `--target` | +| `package` | All | All | āœ… `--target` | +| `upload` | Claude, Gemini, OpenAI | All | āœ… `--target` | +| `estimate` | All | Docs only | No (estimation is universal) | +| `install` | All | All | āœ… `--target` | +| `install-agent` | All | All | No (agent-specific paths) | + +## MCP Tool Support + +| Tool | Platforms | Skill Modes | Multi-Platform Param | +|------|-----------|-------------|----------------------| +| **Config Tools** | +| `generate_config` | All | All | No (creates generic JSON) | +| `list_configs` | All | All | No | +| `validate_config` | All | All | No | +| `fetch_config` | All | All | No | +| **Scraping Tools** | +| `estimate_pages` | All | Docs only | No | +| `scrape_docs` | All | Docs + Unified | No (output is universal) | +| `scrape_github` | All | GitHub only | No (output is universal) | +| `scrape_pdf` | All | PDF only | No (output is universal) | +| **Packaging Tools** | +| `package_skill` | All | All | āœ… `target` parameter | +| `upload_skill` | Claude, Gemini, OpenAI | All | āœ… `target` parameter | +| `enhance_skill` | Claude, Gemini, OpenAI | All | āœ… `target` parameter | +| `install_skill` | All | All | āœ… `target` parameter | +| **Splitting Tools** | +| `split_config` | All | Docs + Unified | No | +| `generate_router` | All | Docs only | No | + +## Feature Comparison by Platform + +### Claude AI (Default) +- **Format:** YAML frontmatter + markdown +- **Package:** ZIP with SKILL.md, references/, scripts/, assets/ +- **Upload:** POST to https://api.anthropic.com/v1/skills +- **Enhancement:** Claude Sonnet 4 (local or API) +- **Unique Features:** MCP integration, Skills API +- **Limitations:** No vector store, no file search + +### Google Gemini +- **Format:** Plain markdown (no frontmatter) +- **Package:** tar.gz with system_instructions.md, references/, metadata +- **Upload:** Google Files API +- **Enhancement:** Gemini 2.0 Flash +- **Unique Features:** Grounding support, long context (1M tokens) +- **Limitations:** tar.gz format only + +### OpenAI ChatGPT +- **Format:** Assistant instructions (plain text) +- **Package:** ZIP with assistant_instructions.txt, vector_store_files/, metadata +- **Upload:** Assistants API + Vector Store creation +- **Enhancement:** GPT-4o +- **Unique Features:** Vector store, file_search tool, semantic search +- **Limitations:** Requires Assistants API structure + +### Generic Markdown +- **Format:** Pure markdown (universal) +- **Package:** ZIP with README.md, DOCUMENTATION.md, references/ +- **Upload:** None (manual distribution) +- **Enhancement:** None +- **Unique Features:** Works with any LLM, no API dependencies +- **Limitations:** No upload, no enhancement + +## Workflow Coverage + +### Single-Source Workflow +``` +Config → Scrape → Build → [Enhance] → Package --target X → [Upload --target X] +``` +**Platforms:** All 4 +**Modes:** Docs, GitHub, PDF + +### Unified Multi-Source Workflow +``` +Config → Scrape All → Detect Conflicts → Merge → Build → [Enhance] → Package --target X → [Upload --target X] +``` +**Platforms:** All 4 +**Modes:** Unified only + +### Complete Installation Workflow +``` +install --target X → Fetch → Scrape → Enhance → Package → Upload +``` +**Platforms:** All 4 +**Modes:** All (via config type detection) + +## API Key Requirements + +| Platform | Environment Variable | Key Format | Required For | +|----------|---------------------|------------|--------------| +| Claude | `ANTHROPIC_API_KEY` | `sk-ant-*` | Upload, API Enhancement | +| Gemini | `GOOGLE_API_KEY` | `AIza*` | Upload, API Enhancement | +| OpenAI | `OPENAI_API_KEY` | `sk-*` | Upload, API Enhancement | +| Markdown | None | N/A | Nothing | + +**Note:** Local enhancement (Claude Code Max) requires no API key for any platform. + +## Installation Options + +```bash +# Core package (Claude only) +pip install skill-seekers + +# With Gemini support +pip install skill-seekers[gemini] + +# With OpenAI support +pip install skill-seekers[openai] + +# With all platforms +pip install skill-seekers[all-llms] +``` + +## Examples + +### Package for Multiple Platforms (Same Skill) +```bash +# Scrape once (platform-agnostic) +skill-seekers scrape --config configs/react.json + +# Package for all platforms +skill-seekers package output/react/ --target claude +skill-seekers package output/react/ --target gemini +skill-seekers package output/react/ --target openai +skill-seekers package output/react/ --target markdown + +# Result: +# - react.zip (Claude) +# - react-gemini.tar.gz (Gemini) +# - react-openai.zip (OpenAI) +# - react-markdown.zip (Universal) +``` + +### Upload to Multiple Platforms +```bash +export ANTHROPIC_API_KEY=sk-ant-... +export GOOGLE_API_KEY=AIzaSy... +export OPENAI_API_KEY=sk-proj-... + +skill-seekers upload react.zip --target claude +skill-seekers upload react-gemini.tar.gz --target gemini +skill-seekers upload react-openai.zip --target openai +``` + +### Use MCP Tools for Any Platform +```python +# In Claude Code or any MCP client + +# Package for Gemini +package_skill(skill_dir="output/react", target="gemini") + +# Upload to OpenAI +upload_skill(skill_zip="output/react-openai.zip", target="openai") + +# Enhance with Gemini +enhance_skill(skill_dir="output/react", target="gemini", mode="api") +``` + +### Complete Workflow with Different Platforms +```bash +# Install React skill for Claude (default) +skill-seekers install --config react + +# Install Django skill for Gemini +skill-seekers install --config django --target gemini + +# Install FastAPI skill for OpenAI +skill-seekers install --config fastapi --target openai + +# Install Vue skill as generic markdown +skill-seekers install --config vue --target markdown +``` + +### Split Unified Config by Source +```bash +# Split multi-source config into separate configs +skill-seekers split --config configs/react_unified.json --strategy source + +# Creates: +# - react-documentation.json (docs only) +# - react-github.json (GitHub only) + +# Then scrape each separately +skill-seekers unified --config react-documentation.json +skill-seekers unified --config react-github.json + +# Or scrape in parallel for speed +skill-seekers unified --config react-documentation.json & +skill-seekers unified --config react-github.json & +wait +``` + +## Verification Checklist + +Before release, verify all combinations: + +### CLI Commands Ɨ Platforms +- [ ] scrape → package claude → upload claude +- [ ] scrape → package gemini → upload gemini +- [ ] scrape → package openai → upload openai +- [ ] scrape → package markdown +- [ ] github → package (all platforms) +- [ ] pdf → package (all platforms) +- [ ] unified → package (all platforms) +- [ ] enhance claude +- [ ] enhance gemini +- [ ] enhance openai + +### MCP Tools Ɨ Platforms +- [ ] package_skill target=claude +- [ ] package_skill target=gemini +- [ ] package_skill target=openai +- [ ] package_skill target=markdown +- [ ] upload_skill target=claude +- [ ] upload_skill target=gemini +- [ ] upload_skill target=openai +- [ ] enhance_skill target=claude +- [ ] enhance_skill target=gemini +- [ ] enhance_skill target=openai +- [ ] install_skill target=claude +- [ ] install_skill target=gemini +- [ ] install_skill target=openai + +### Skill Modes Ɨ Platforms +- [ ] Docs → Claude +- [ ] Docs → Gemini +- [ ] Docs → OpenAI +- [ ] Docs → Markdown +- [ ] GitHub → All platforms +- [ ] PDF → All platforms +- [ ] Unified → All platforms +- [ ] Local Repo → All platforms + +## Platform-Specific Notes + +### Claude AI +- **Best for:** General-purpose skills, MCP integration +- **When to use:** Default choice, best MCP support +- **File size limit:** 25 MB per skill package + +### Google Gemini +- **Best for:** Large context skills, grounding support +- **When to use:** Need long context (1M tokens), grounding features +- **File size limit:** 100 MB per upload + +### OpenAI ChatGPT +- **Best for:** Vector search, semantic retrieval +- **When to use:** Need semantic search across documentation +- **File size limit:** 512 MB per vector store + +### Generic Markdown +- **Best for:** Universal compatibility, no API dependencies +- **When to use:** Using non-Claude/Gemini/OpenAI LLMs, offline use +- **Distribution:** Manual - share ZIP file directly + +## Frequently Asked Questions + +**Q: Can I package once and upload to multiple platforms?** +A: No. Each platform requires a platform-specific package format. You must: +1. Scrape once (universal) +2. Package separately for each platform (`--target` flag) +3. Upload each platform-specific package + +**Q: Do I need to scrape separately for each platform?** +A: No! Scraping is platform-agnostic. Scrape once, then package for multiple platforms. + +**Q: Which platform should I choose?** +A: +- **Claude:** Best default choice, excellent MCP integration +- **Gemini:** Choose if you need long context (1M tokens) or grounding +- **OpenAI:** Choose if you need vector search and semantic retrieval +- **Markdown:** Choose for universal compatibility or offline use + +**Q: Can I enhance a skill for different platforms?** +A: Yes! Enhancement adds platform-specific formatting: +- Claude: YAML frontmatter + markdown +- Gemini: Plain markdown with system instructions +- OpenAI: Plain text assistant instructions + +**Q: Do all skill modes work with all platforms?** +A: Yes! All 5 skill modes (Docs, GitHub, PDF, Unified, Local Repo) work with all 4 platforms. + +## See Also + +- **[README.md](../README.md)** - Complete user documentation +- **[UNIFIED_SCRAPING.md](UNIFIED_SCRAPING.md)** - Multi-source scraping guide +- **[ENHANCEMENT.md](ENHANCEMENT.md)** - AI enhancement guide +- **[UPLOAD_GUIDE.md](UPLOAD_GUIDE.md)** - Upload instructions +- **[MCP_SETUP.md](MCP_SETUP.md)** - MCP server setup diff --git a/src/skill_seekers/cli/install_skill.py b/src/skill_seekers/cli/install_skill.py index 8298e5d..0a49a48 100644 --- a/src/skill_seekers/cli/install_skill.py +++ b/src/skill_seekers/cli/install_skill.py @@ -60,17 +60,24 @@ Examples: # Preview workflow (dry run) skill-seekers install --config react --dry-run + # Install for Gemini instead of Claude + skill-seekers install --config react --target gemini + + # Install for OpenAI ChatGPT + skill-seekers install --config fastapi --target openai + Important: - Enhancement is MANDATORY (30-60 sec) for quality (3/10→9/10) - Total time: 20-45 minutes (mostly scraping) - - Auto-uploads to Claude if ANTHROPIC_API_KEY is set + - Multi-platform support: claude (default), gemini, openai, markdown + - Auto-uploads if API key is set (ANTHROPIC_API_KEY, GOOGLE_API_KEY, or OPENAI_API_KEY) Phases: 1. Fetch config (if config name provided) 2. Scrape documentation 3. AI Enhancement (MANDATORY - no skip option) - 4. Package to .zip - 5. Upload to Claude (optional) + 4. Package for target platform (ZIP or tar.gz) + 5. Upload to target platform (optional) """ ) @@ -104,6 +111,13 @@ Phases: help="Preview workflow without executing" ) + parser.add_argument( + "--target", + choices=['claude', 'gemini', 'openai', 'markdown'], + default='claude', + help="Target LLM platform (default: claude)" + ) + args = parser.parse_args() # Determine if config is a name or path @@ -124,7 +138,8 @@ Phases: "destination": args.destination, "auto_upload": not args.no_upload, "unlimited": args.unlimited, - "dry_run": args.dry_run + "dry_run": args.dry_run, + "target": args.target } # Run async tool diff --git a/src/skill_seekers/cli/split_config.py b/src/skill_seekers/cli/split_config.py index 40551ad..c452bb7 100644 --- a/src/skill_seekers/cli/split_config.py +++ b/src/skill_seekers/cli/split_config.py @@ -36,15 +36,37 @@ class ConfigSplitter: print(f"āŒ Error: Invalid JSON in config file: {e}") sys.exit(1) + def is_unified_config(self) -> bool: + """Check if this is a unified multi-source config""" + return 'sources' in self.config + def get_split_strategy(self) -> str: """Determine split strategy""" - # Check if strategy is defined in config + # For unified configs, default to source-based splitting + if self.is_unified_config(): + if self.strategy == "auto": + num_sources = len(self.config.get('sources', [])) + if num_sources <= 1: + print(f"ā„¹ļø Single source unified config - no splitting needed") + return "none" + else: + print(f"ā„¹ļø Multi-source unified config ({num_sources} sources) - source split recommended") + return "source" + # For unified configs, only 'source' and 'none' strategies are valid + elif self.strategy in ['source', 'none']: + return self.strategy + else: + print(f"āš ļø Warning: Strategy '{self.strategy}' not supported for unified configs") + print(f"ā„¹ļø Using 'source' strategy instead") + return "source" + + # Check if strategy is defined in config (documentation configs) if 'split_strategy' in self.config: config_strategy = self.config['split_strategy'] if config_strategy != "none": return config_strategy - # Use provided strategy or auto-detect + # Use provided strategy or auto-detect (documentation configs) if self.strategy == "auto": max_pages = self.config.get('max_pages', 500) @@ -147,6 +169,46 @@ class ConfigSplitter: print(f"āœ… Created {len(configs)} size-based configs ({self.target_pages} pages each)") return configs + def split_by_source(self) -> List[Dict[str, Any]]: + """Split unified config by source type""" + if not self.is_unified_config(): + print("āŒ Error: Config is not a unified config (missing 'sources' key)") + sys.exit(1) + + sources = self.config.get('sources', []) + if not sources: + print("āŒ Error: No sources defined in unified config") + sys.exit(1) + + configs = [] + source_type_counts = defaultdict(int) + + for source in sources: + source_type = source.get('type', 'unknown') + source_type_counts[source_type] += 1 + count = source_type_counts[source_type] + + # Create new config for this source + new_config = { + 'name': f"{self.base_name}-{source_type}" + (f"-{count}" if count > 1 else ""), + 'description': f"{self.base_name.capitalize()} - {source_type.title()} source. {self.config.get('description', '')}", + 'sources': [source] # Single source per config + } + + # Copy merge_mode if it exists + if 'merge_mode' in self.config: + new_config['merge_mode'] = self.config['merge_mode'] + + configs.append(new_config) + + print(f"āœ… Created {len(configs)} source-based configs") + + # Show breakdown by source type + for source_type, count in source_type_counts.items(): + print(f" šŸ“„ {count}x {source_type}") + + return configs + def create_router_config(self, sub_configs: List[Dict[str, Any]]) -> Dict[str, Any]: """Create a router config that references sub-skills""" router_name = self.config.get('split_config', {}).get('router_name', self.base_name) @@ -173,17 +235,22 @@ class ConfigSplitter: """Execute split based on strategy""" strategy = self.get_split_strategy() + config_type = "UNIFIED" if self.is_unified_config() else "DOCUMENTATION" print(f"\n{'='*60}") - print(f"CONFIG SPLITTER: {self.base_name}") + print(f"CONFIG SPLITTER: {self.base_name} ({config_type})") print(f"{'='*60}") print(f"Strategy: {strategy}") - print(f"Target pages per skill: {self.target_pages}") + if not self.is_unified_config(): + print(f"Target pages per skill: {self.target_pages}") print("") if strategy == "none": print("ā„¹ļø No splitting required") return [self.config] + elif strategy == "source": + return self.split_by_source() + elif strategy == "category": return self.split_by_category(create_router=False) @@ -245,9 +312,14 @@ Examples: Split Strategies: none - No splitting (single skill) auto - Automatically choose best strategy + source - Split unified configs by source type (docs, github, pdf) category - Split by categories defined in config router - Create router + category-based sub-skills size - Split by page count + +Config Types: + Documentation - Single base_url config (supports: category, router, size) + Unified - Multi-source config (supports: source) """ ) @@ -258,7 +330,7 @@ Split Strategies: parser.add_argument( '--strategy', - choices=['auto', 'none', 'category', 'router', 'size'], + choices=['auto', 'none', 'source', 'category', 'router', 'size'], default='auto', help='Splitting strategy (default: auto)' ) diff --git a/src/skill_seekers/mcp/server_fastmcp.py b/src/skill_seekers/mcp/server_fastmcp.py index b8380df..49bf9cc 100644 --- a/src/skill_seekers/mcp/server_fastmcp.py +++ b/src/skill_seekers/mcp/server_fastmcp.py @@ -84,6 +84,7 @@ try: # Packaging tools package_skill_impl, upload_skill_impl, + enhance_skill_impl, install_skill_impl, # Splitting tools split_config_impl, @@ -109,6 +110,7 @@ except ImportError: scrape_pdf_impl, package_skill_impl, upload_skill_impl, + enhance_skill_impl, install_skill_impl, split_config_impl, generate_router_impl, @@ -397,24 +399,27 @@ async def scrape_pdf( @safe_tool_decorator( - description="Package a skill directory into a .zip file ready for Claude upload. Automatically uploads if ANTHROPIC_API_KEY is set." + description="Package skill directory into platform-specific format (ZIP for Claude/OpenAI/Markdown, tar.gz for Gemini). Supports all platforms: claude, gemini, openai, markdown. Automatically uploads if platform API key is set." ) async def package_skill( skill_dir: str, + target: str = "claude", auto_upload: bool = True, ) -> str: """ - Package a skill directory into a .zip file. + Package skill directory for target LLM platform. Args: - skill_dir: Path to skill directory (e.g., output/react/) - auto_upload: Try to upload automatically if API key is available (default: true). If false, only package without upload attempt. + skill_dir: Path to skill directory to package (e.g., output/react/) + target: Target platform (default: 'claude'). Options: claude, gemini, openai, markdown + auto_upload: Auto-upload after packaging if API key is available (default: true). Requires platform-specific API key: ANTHROPIC_API_KEY, GOOGLE_API_KEY, or OPENAI_API_KEY. Returns: - Packaging results with .zip file path and upload status. + Packaging results with file path and platform info. """ args = { "skill_dir": skill_dir, + "target": target, "auto_upload": auto_upload, } result = await package_skill_impl(args) @@ -424,26 +429,74 @@ async def package_skill( @safe_tool_decorator( - description="Upload a skill .zip file to Claude automatically (requires ANTHROPIC_API_KEY)" + description="Upload skill package to target LLM platform API. Requires platform-specific API key. Supports: claude (Anthropic Skills API), gemini (Google Files API), openai (Assistants API). Does NOT support markdown." ) -async def upload_skill(skill_zip: str) -> str: +async def upload_skill( + skill_zip: str, + target: str = "claude", + api_key: str | None = None, +) -> str: """ - Upload a skill .zip file to Claude. + Upload skill package to target platform. Args: - skill_zip: Path to skill .zip file (e.g., output/react.zip) + skill_zip: Path to skill package (.zip or .tar.gz, e.g., output/react.zip) + target: Target platform (default: 'claude'). Options: claude, gemini, openai + api_key: Optional API key (uses env var if not provided: ANTHROPIC_API_KEY, GOOGLE_API_KEY, or OPENAI_API_KEY) Returns: - Upload results with success/error message. + Upload results with skill ID and platform URL. """ - result = await upload_skill_impl({"skill_zip": skill_zip}) + args = { + "skill_zip": skill_zip, + "target": target, + } + if api_key: + args["api_key"] = api_key + + result = await upload_skill_impl(args) if isinstance(result, list) and result: return result[0].text if hasattr(result[0], "text") else str(result[0]) return str(result) @safe_tool_decorator( - description="Complete one-command workflow: fetch config → scrape docs → AI enhance (MANDATORY) → package → upload. Enhancement required for quality (3/10→9/10). Takes 20-45 min depending on config size. Automatically uploads to Claude if ANTHROPIC_API_KEY is set." + description="Enhance SKILL.md with AI using target platform's model. Local mode uses Claude Code Max (no API key). API mode uses platform API (requires key). Transforms basic templates into comprehensive 500+ line guides with examples." +) +async def enhance_skill( + skill_dir: str, + target: str = "claude", + mode: str = "local", + api_key: str | None = None, +) -> str: + """ + Enhance SKILL.md with AI. + + Args: + skill_dir: Path to skill directory containing SKILL.md (e.g., output/react/) + target: Target platform (default: 'claude'). Options: claude, gemini, openai + mode: Enhancement mode (default: 'local'). Options: local (Claude Code, no API), api (uses platform API) + api_key: Optional API key for 'api' mode (uses env var if not provided: ANTHROPIC_API_KEY, GOOGLE_API_KEY, or OPENAI_API_KEY) + + Returns: + Enhancement results with backup location. + """ + args = { + "skill_dir": skill_dir, + "target": target, + "mode": mode, + } + if api_key: + args["api_key"] = api_key + + result = await enhance_skill_impl(args) + if isinstance(result, list) and result: + return result[0].text if hasattr(result[0], "text") else str(result[0]) + return str(result) + + +@safe_tool_decorator( + description="Complete one-command workflow: fetch config → scrape docs → AI enhance (MANDATORY) → package → upload. Enhancement required for quality (3/10→9/10). Takes 20-45 min depending on config size. Supports multiple LLM platforms: claude (default), gemini, openai, markdown. Auto-uploads if platform API key is set." ) async def install_skill( config_name: str | None = None, @@ -452,6 +505,7 @@ async def install_skill( auto_upload: bool = True, unlimited: bool = False, dry_run: bool = False, + target: str = "claude", ) -> str: """ Complete one-command workflow to install a skill. @@ -460,9 +514,10 @@ async def install_skill( config_name: Config name from API (e.g., 'react', 'django'). Mutually exclusive with config_path. Tool will fetch this config from the official API before scraping. config_path: Path to existing config JSON file (e.g., 'configs/custom.json'). Mutually exclusive with config_name. Use this if you already have a config file. destination: Output directory for skill files (default: 'output') - auto_upload: Auto-upload to Claude after packaging (requires ANTHROPIC_API_KEY). Default: true. Set to false to skip upload. + auto_upload: Auto-upload after packaging (requires platform API key). Default: true. Set to false to skip upload. unlimited: Remove page limits during scraping (default: false). WARNING: Can take hours for large sites. dry_run: Preview workflow without executing (default: false). Shows all phases that would run. + target: Target LLM platform (default: 'claude'). Options: claude, gemini, openai, markdown. Requires corresponding API key: ANTHROPIC_API_KEY, GOOGLE_API_KEY, or OPENAI_API_KEY. Returns: Workflow results with all phase statuses. @@ -472,6 +527,7 @@ async def install_skill( "auto_upload": auto_upload, "unlimited": unlimited, "dry_run": dry_run, + "target": target, } if config_name: args["config_name"] = config_name @@ -490,7 +546,7 @@ async def install_skill( @safe_tool_decorator( - description="Split large documentation config into multiple focused skills. For 10K+ page documentation." + description="Split large configs into multiple focused skills. Supports documentation (10K+ pages) and unified multi-source configs. Auto-detects config type and recommends best strategy." ) async def split_config( config_path: str, @@ -499,12 +555,16 @@ async def split_config( dry_run: bool = False, ) -> str: """ - Split large documentation config into multiple skills. + Split large configs into multiple skills. + + Supports: + - Documentation configs: Split by categories, size, or create router skills + - Unified configs: Split by source type (documentation, github, pdf) Args: - config_path: Path to config JSON file (e.g., configs/godot.json) - strategy: Split strategy: auto, none, category, router, size (default: auto) - target_pages: Target pages per skill (default: 5000) + config_path: Path to config JSON file (e.g., configs/godot.json or configs/react_unified.json) + strategy: Split strategy: auto, none, source, category, router, size (default: auto). 'source' is for unified configs. + target_pages: Target pages per skill for doc configs (default: 5000) dry_run: Preview without saving files (default: false) Returns: diff --git a/src/skill_seekers/mcp/tools/__init__.py b/src/skill_seekers/mcp/tools/__init__.py index 20ac57d..2abced9 100644 --- a/src/skill_seekers/mcp/tools/__init__.py +++ b/src/skill_seekers/mcp/tools/__init__.py @@ -29,6 +29,7 @@ from .scraping_tools import ( from .packaging_tools import ( package_skill_tool as package_skill_impl, upload_skill_tool as upload_skill_impl, + enhance_skill_tool as enhance_skill_impl, install_skill_tool as install_skill_impl, ) @@ -58,6 +59,7 @@ __all__ = [ # Packaging tools "package_skill_impl", "upload_skill_impl", + "enhance_skill_impl", "install_skill_impl", # Splitting tools "split_config_impl", diff --git a/src/skill_seekers/mcp/tools/packaging_tools.py b/src/skill_seekers/mcp/tools/packaging_tools.py index 7172de1..c3421a7 100644 --- a/src/skill_seekers/mcp/tools/packaging_tools.py +++ b/src/skill_seekers/mcp/tools/packaging_tools.py @@ -102,30 +102,46 @@ def run_subprocess_with_streaming(cmd: List[str], timeout: int = None) -> Tuple[ async def package_skill_tool(args: dict) -> List[TextContent]: """ - Package skill to .zip and optionally auto-upload. + Package skill for target LLM platform and optionally auto-upload. Args: args: Dictionary with: - skill_dir (str): Path to skill directory (e.g., output/react/) - auto_upload (bool): Try to upload automatically if API key is available (default: True) + - target (str): Target platform (default: 'claude') + Options: 'claude', 'gemini', 'openai', 'markdown' Returns: List of TextContent with packaging results """ + from skill_seekers.cli.adaptors import get_adaptor + skill_dir = args["skill_dir"] auto_upload = args.get("auto_upload", True) + target = args.get("target", "claude") - # Check if API key exists - only upload if available - has_api_key = os.environ.get('ANTHROPIC_API_KEY', '').strip() + # Get platform adaptor + try: + adaptor = get_adaptor(target) + except ValueError as e: + return [TextContent( + type="text", + text=f"āŒ Invalid platform: {str(e)}\n\nSupported platforms: claude, gemini, openai, markdown" + )] + + # Check if platform-specific API key exists - only upload if available + env_var_name = adaptor.get_env_var_name() + has_api_key = os.environ.get(env_var_name, '').strip() if env_var_name else False should_upload = auto_upload and has_api_key - # Run package_skill.py + # Run package_skill.py with target parameter cmd = [ sys.executable, str(CLI_DIR / "package_skill.py"), skill_dir, "--no-open", # Don't open folder in MCP context - "--skip-quality-check" # Skip interactive quality checks in MCP context + "--skip-quality-check", # Skip interactive quality checks in MCP context + "--target", target # Add target platform ] # Add upload flag only if we have API key @@ -135,9 +151,9 @@ async def package_skill_tool(args: dict) -> List[TextContent]: # Timeout: 5 minutes for packaging + upload timeout = 300 - progress_msg = "šŸ“¦ Packaging skill...\n" + progress_msg = f"šŸ“¦ Packaging skill for {adaptor.PLATFORM_NAME}...\n" if should_upload: - progress_msg += "šŸ“¤ Will auto-upload if successful\n" + progress_msg += f"šŸ“¤ Will auto-upload to {adaptor.PLATFORM_NAME} if successful\n" progress_msg += f"ā±ļø Maximum time: {timeout // 60} minutes\n\n" stdout, stderr, returncode = run_subprocess_with_streaming(cmd, timeout=timeout) @@ -147,24 +163,54 @@ async def package_skill_tool(args: dict) -> List[TextContent]: if returncode == 0: if should_upload: # Upload succeeded - output += "\n\nāœ… Skill packaged and uploaded automatically!" - output += "\n Your skill is now available in Claude!" + output += f"\n\nāœ… Skill packaged and uploaded to {adaptor.PLATFORM_NAME}!" + if target == 'claude': + output += "\n Your skill is now available in Claude!" + output += "\n Go to https://claude.ai/skills to use it" + elif target == 'gemini': + output += "\n Your skill is now available in Gemini!" + output += "\n Go to https://aistudio.google.com/ to use it" + elif target == 'openai': + output += "\n Your assistant is now available in OpenAI!" + output += "\n Go to https://platform.openai.com/assistants/ to use it" elif auto_upload and not has_api_key: # User wanted upload but no API key - output += "\n\nšŸ“ Skill packaged successfully!" + output += f"\n\nšŸ“ Skill packaged successfully for {adaptor.PLATFORM_NAME}!" output += "\n" output += "\nšŸ’” To enable automatic upload:" - output += "\n 1. Get API key from https://console.anthropic.com/" - output += "\n 2. Set: export ANTHROPIC_API_KEY=sk-ant-..." - output += "\n" - output += "\nšŸ“¤ Manual upload:" - output += "\n 1. Find the .zip file in your output/ folder" - output += "\n 2. Go to https://claude.ai/skills" - output += "\n 3. Click 'Upload Skill' and select the .zip file" + if target == 'claude': + output += "\n 1. Get API key from https://console.anthropic.com/" + output += "\n 2. Set: export ANTHROPIC_API_KEY=sk-ant-..." + output += "\n\nšŸ“¤ Manual upload:" + output += "\n 1. Find the .zip file in your output/ folder" + output += "\n 2. Go to https://claude.ai/skills" + output += "\n 3. Click 'Upload Skill' and select the .zip file" + elif target == 'gemini': + output += "\n 1. Get API key from https://aistudio.google.com/" + output += "\n 2. Set: export GOOGLE_API_KEY=AIza..." + output += "\n\nšŸ“¤ Manual upload:" + output += "\n 1. Go to https://aistudio.google.com/" + output += "\n 2. Upload the .tar.gz file from your output/ folder" + elif target == 'openai': + output += "\n 1. Get API key from https://platform.openai.com/" + output += "\n 2. Set: export OPENAI_API_KEY=sk-proj-..." + output += "\n\nšŸ“¤ Manual upload:" + output += "\n 1. Use OpenAI Assistants API" + output += "\n 2. Upload the .zip file from your output/ folder" + elif target == 'markdown': + output += "\n (No API key needed - markdown is export only)" + output += "\n Package created for manual distribution" else: # auto_upload=False, just packaged - output += "\n\nāœ… Skill packaged successfully!" - output += "\n Upload manually to https://claude.ai/skills" + output += f"\n\nāœ… Skill packaged successfully for {adaptor.PLATFORM_NAME}!" + if target == 'claude': + output += "\n Upload manually to https://claude.ai/skills" + elif target == 'gemini': + output += "\n Upload manually to https://aistudio.google.com/" + elif target == 'openai': + output += "\n Upload manually via OpenAI Assistants API" + elif target == 'markdown': + output += "\n Package ready for manual distribution" return [TextContent(type="text", text=output)] else: @@ -173,28 +219,57 @@ async def package_skill_tool(args: dict) -> List[TextContent]: async def upload_skill_tool(args: dict) -> List[TextContent]: """ - Upload skill .zip to Claude. + Upload skill package to target LLM platform. Args: args: Dictionary with: - - skill_zip (str): Path to skill .zip file (e.g., output/react.zip) + - skill_zip (str): Path to skill package (.zip or .tar.gz) + - target (str): Target platform (default: 'claude') + Options: 'claude', 'gemini', 'openai' + Note: 'markdown' does not support upload + - api_key (str, optional): API key (uses env var if not provided) Returns: List of TextContent with upload results """ - skill_zip = args["skill_zip"] + from skill_seekers.cli.adaptors import get_adaptor - # Run upload_skill.py + skill_zip = args["skill_zip"] + target = args.get("target", "claude") + api_key = args.get("api_key") + + # Get platform adaptor + try: + adaptor = get_adaptor(target) + except ValueError as e: + return [TextContent( + type="text", + text=f"āŒ Invalid platform: {str(e)}\n\nSupported platforms: claude, gemini, openai" + )] + + # Check if upload is supported + if target == 'markdown': + return [TextContent( + type="text", + text="āŒ Markdown export does not support upload. Use the packaged file manually." + )] + + # Run upload_skill.py with target parameter cmd = [ sys.executable, str(CLI_DIR / "upload_skill.py"), - skill_zip + skill_zip, + "--target", target ] + # Add API key if provided + if api_key: + cmd.extend(["--api-key", api_key]) + # Timeout: 5 minutes for upload timeout = 300 - progress_msg = "šŸ“¤ Uploading skill to Claude...\n" + progress_msg = f"šŸ“¤ Uploading skill to {adaptor.PLATFORM_NAME}...\n" progress_msg += f"ā±ļø Maximum time: {timeout // 60} minutes\n\n" stdout, stderr, returncode = run_subprocess_with_streaming(cmd, timeout=timeout) @@ -207,6 +282,142 @@ async def upload_skill_tool(args: dict) -> List[TextContent]: return [TextContent(type="text", text=f"{output}\n\nāŒ Error:\n{stderr}")] +async def enhance_skill_tool(args: dict) -> List[TextContent]: + """ + Enhance SKILL.md with AI using target platform's model. + + Args: + args: Dictionary with: + - skill_dir (str): Path to skill directory + - target (str): Target platform (default: 'claude') + Options: 'claude', 'gemini', 'openai' + Note: 'markdown' does not support enhancement + - mode (str): Enhancement mode (default: 'local') + 'local': Uses Claude Code Max (no API key) + 'api': Uses platform API (requires API key) + - api_key (str, optional): API key for 'api' mode + + Returns: + List of TextContent with enhancement results + """ + from skill_seekers.cli.adaptors import get_adaptor + + skill_dir = Path(args.get("skill_dir")) + target = args.get("target", "claude") + mode = args.get("mode", "local") + api_key = args.get("api_key") + + # Validate skill directory + if not skill_dir.exists(): + return [TextContent( + type="text", + text=f"āŒ Skill directory not found: {skill_dir}" + )] + + if not (skill_dir / "SKILL.md").exists(): + return [TextContent( + type="text", + text=f"āŒ SKILL.md not found in {skill_dir}" + )] + + # Get platform adaptor + try: + adaptor = get_adaptor(target) + except ValueError as e: + return [TextContent( + type="text", + text=f"āŒ Invalid platform: {str(e)}\n\nSupported platforms: claude, gemini, openai" + )] + + # Check if enhancement is supported + if not adaptor.supports_enhancement(): + return [TextContent( + type="text", + text=f"āŒ {adaptor.PLATFORM_NAME} does not support AI enhancement" + )] + + output_lines = [] + output_lines.append(f"šŸš€ Enhancing skill with {adaptor.PLATFORM_NAME}") + output_lines.append("-" * 70) + output_lines.append(f"Skill directory: {skill_dir}") + output_lines.append(f"Mode: {mode}") + output_lines.append("") + + if mode == 'local': + # Use local enhancement (Claude Code) + output_lines.append("Using Claude Code Max (local, no API key required)") + output_lines.append("Running enhancement in headless mode...") + output_lines.append("") + + cmd = [ + sys.executable, + str(CLI_DIR / "enhance_skill_local.py"), + str(skill_dir) + ] + + try: + stdout, stderr, returncode = run_subprocess_with_streaming(cmd, timeout=900) + + if returncode == 0: + output_lines.append(stdout) + output_lines.append("") + output_lines.append("āœ… Enhancement complete!") + output_lines.append(f"Enhanced SKILL.md: {skill_dir / 'SKILL.md'}") + output_lines.append(f"Backup: {skill_dir / 'SKILL.md.backup'}") + else: + output_lines.append(f"āŒ Enhancement failed (exit code {returncode})") + output_lines.append(stderr if stderr else stdout) + + except Exception as e: + output_lines.append(f"āŒ Error: {str(e)}") + + elif mode == 'api': + # Use API enhancement + output_lines.append(f"Using {adaptor.PLATFORM_NAME} API") + + # Get API key + if not api_key: + env_var = adaptor.get_env_var_name() + api_key = os.environ.get(env_var) + + if not api_key: + return [TextContent( + type="text", + text=f"āŒ {env_var} not set. Set API key or pass via api_key parameter." + )] + + # Validate API key + if not adaptor.validate_api_key(api_key): + return [TextContent( + type="text", + text=f"āŒ Invalid API key format for {adaptor.PLATFORM_NAME}" + )] + + output_lines.append("Calling API for enhancement...") + output_lines.append("") + + try: + success = adaptor.enhance(skill_dir, api_key) + + if success: + output_lines.append("āœ… Enhancement complete!") + output_lines.append(f"Enhanced SKILL.md: {skill_dir / 'SKILL.md'}") + output_lines.append(f"Backup: {skill_dir / 'SKILL.md.backup'}") + else: + output_lines.append("āŒ Enhancement failed") + + except Exception as e: + output_lines.append(f"āŒ Error: {str(e)}") + + else: + return [TextContent( + type="text", + text=f"āŒ Invalid mode: {mode}. Use 'local' or 'api'" + )] + + return [TextContent(type="text", text="\n".join(output_lines))] + + async def install_skill_tool(args: dict) -> List[TextContent]: """ Complete skill installation workflow. @@ -215,8 +426,8 @@ async def install_skill_tool(args: dict) -> List[TextContent]: 1. Fetch config (if config_name provided) 2. Scrape documentation 3. AI Enhancement (MANDATORY - no skip option) - 4. Package to .zip - 5. Upload to Claude (optional) + 4. Package for target platform (ZIP or tar.gz) + 5. Upload to target platform (optional) Args: args: Dictionary with: @@ -226,13 +437,15 @@ async def install_skill_tool(args: dict) -> List[TextContent]: - auto_upload (bool): Upload after packaging (default: True) - unlimited (bool): Remove page limits (default: False) - dry_run (bool): Preview only (default: False) + - target (str): Target LLM platform (default: "claude") Returns: List of TextContent with workflow progress and results """ # Import these here to avoid circular imports from .scraping_tools import scrape_docs_tool - from .config_tools import fetch_config_tool + from .source_tools import fetch_config_tool + from skill_seekers.cli.adaptors import get_adaptor # Extract and validate inputs config_name = args.get("config_name") @@ -241,6 +454,16 @@ async def install_skill_tool(args: dict) -> List[TextContent]: auto_upload = args.get("auto_upload", True) unlimited = args.get("unlimited", False) dry_run = args.get("dry_run", False) + target = args.get("target", "claude") + + # Get platform adaptor + try: + adaptor = get_adaptor(target) + except ValueError as e: + return [TextContent( + type="text", + text=f"āŒ Error: {str(e)}\n\nSupported platforms: claude, gemini, openai, markdown" + )] # Validation: Must provide exactly one of config_name or config_path if not config_name and not config_path: @@ -397,73 +620,118 @@ async def install_skill_tool(args: dict) -> List[TextContent]: # ===== PHASE 4: Package Skill ===== phase_num = "4/5" if config_name else "3/4" - output_lines.append(f"šŸ“¦ PHASE {phase_num}: Package Skill") + output_lines.append(f"šŸ“¦ PHASE {phase_num}: Package Skill for {adaptor.PLATFORM_NAME}") output_lines.append("-" * 70) output_lines.append(f"Skill directory: {workflow_state['skill_dir']}") + output_lines.append(f"Target platform: {adaptor.PLATFORM_NAME}") output_lines.append("") if not dry_run: - # Call package_skill_tool (auto_upload=False, we handle upload separately) + # Call package_skill_tool with target package_result = await package_skill_tool({ "skill_dir": workflow_state['skill_dir'], - "auto_upload": False # We handle upload in next phase + "auto_upload": False, # We handle upload in next phase + "target": target }) package_output = package_result[0].text output_lines.append(package_output) output_lines.append("") - # Extract zip path from output - # Expected format: "Saved to: output/react.zip" - match = re.search(r"Saved to:\s*(.+\.zip)", package_output) + # Extract package path from output (supports .zip and .tar.gz) + # Expected format: "Saved to: output/react.zip" or "Saved to: output/react-gemini.tar.gz" + match = re.search(r"Saved to:\s*(.+\.(?:zip|tar\.gz))", package_output) if match: workflow_state['zip_path'] = match.group(1).strip() else: - # Fallback: construct zip path - workflow_state['zip_path'] = f"{destination}/{workflow_state['skill_name']}.zip" + # Fallback: construct package path based on platform + if target == 'gemini': + workflow_state['zip_path'] = f"{destination}/{workflow_state['skill_name']}-gemini.tar.gz" + elif target == 'openai': + workflow_state['zip_path'] = f"{destination}/{workflow_state['skill_name']}-openai.zip" + else: + workflow_state['zip_path'] = f"{destination}/{workflow_state['skill_name']}.zip" workflow_state['phases_completed'].append('package_skill') else: - output_lines.append(" [DRY RUN] Would package to .zip file") - workflow_state['zip_path'] = f"{destination}/{workflow_state['skill_name']}.zip" + # Dry run - show expected package format + if target == 'gemini': + pkg_ext = "tar.gz" + pkg_file = f"{destination}/{workflow_state['skill_name']}-gemini.tar.gz" + elif target == 'openai': + pkg_ext = "zip" + pkg_file = f"{destination}/{workflow_state['skill_name']}-openai.zip" + else: + pkg_ext = "zip" + pkg_file = f"{destination}/{workflow_state['skill_name']}.zip" + + output_lines.append(f" [DRY RUN] Would package to {pkg_ext} file for {adaptor.PLATFORM_NAME}") + workflow_state['zip_path'] = pkg_file output_lines.append("") # ===== PHASE 5: Upload (Optional) ===== if auto_upload: phase_num = "5/5" if config_name else "4/4" - output_lines.append(f"šŸ“¤ PHASE {phase_num}: Upload to Claude") + output_lines.append(f"šŸ“¤ PHASE {phase_num}: Upload to {adaptor.PLATFORM_NAME}") output_lines.append("-" * 70) - output_lines.append(f"Zip file: {workflow_state['zip_path']}") + output_lines.append(f"Package file: {workflow_state['zip_path']}") output_lines.append("") - # Check for API key - has_api_key = os.environ.get('ANTHROPIC_API_KEY', '').strip() + # Check for platform-specific API key + env_var_name = adaptor.get_env_var_name() + has_api_key = os.environ.get(env_var_name, '').strip() if not dry_run: if has_api_key: - # Call upload_skill_tool - upload_result = await upload_skill_tool({ - "skill_zip": workflow_state['zip_path'] - }) + # Upload not supported for markdown platform + if target == 'markdown': + output_lines.append("āš ļø Markdown export does not support upload") + output_lines.append(" Package has been created - use manually") + else: + # Call upload_skill_tool with target + upload_result = await upload_skill_tool({ + "skill_zip": workflow_state['zip_path'], + "target": target + }) - upload_output = upload_result[0].text - output_lines.append(upload_output) + upload_output = upload_result[0].text + output_lines.append(upload_output) - workflow_state['phases_completed'].append('upload_skill') + workflow_state['phases_completed'].append('upload_skill') else: - output_lines.append("āš ļø ANTHROPIC_API_KEY not set - skipping upload") + # Platform-specific instructions for missing API key + output_lines.append(f"āš ļø {env_var_name} not set - skipping upload") output_lines.append("") output_lines.append("To enable automatic upload:") - output_lines.append(" 1. Get API key from https://console.anthropic.com/") - output_lines.append(" 2. Set: export ANTHROPIC_API_KEY=sk-ant-...") - output_lines.append("") - output_lines.append("šŸ“¤ Manual upload:") - output_lines.append(" 1. Go to https://claude.ai/skills") - output_lines.append(" 2. Click 'Upload Skill'") - output_lines.append(f" 3. Select: {workflow_state['zip_path']}") + + if target == 'claude': + output_lines.append(" 1. Get API key from https://console.anthropic.com/") + output_lines.append(" 2. Set: export ANTHROPIC_API_KEY=sk-ant-...") + output_lines.append("") + output_lines.append("šŸ“¤ Manual upload:") + output_lines.append(" 1. Go to https://claude.ai/skills") + output_lines.append(" 2. Click 'Upload Skill'") + output_lines.append(f" 3. Select: {workflow_state['zip_path']}") + elif target == 'gemini': + output_lines.append(" 1. Get API key from https://aistudio.google.com/") + output_lines.append(" 2. Set: export GOOGLE_API_KEY=AIza...") + output_lines.append("") + output_lines.append("šŸ“¤ Manual upload:") + output_lines.append(" 1. Go to https://aistudio.google.com/") + output_lines.append(f" 2. Upload package: {workflow_state['zip_path']}") + elif target == 'openai': + output_lines.append(" 1. Get API key from https://platform.openai.com/") + output_lines.append(" 2. Set: export OPENAI_API_KEY=sk-proj-...") + output_lines.append("") + output_lines.append("šŸ“¤ Manual upload:") + output_lines.append(" 1. Use OpenAI Assistants API") + output_lines.append(f" 2. Upload package: {workflow_state['zip_path']}") + elif target == 'markdown': + output_lines.append(" (No API key needed - markdown is export only)") + output_lines.append(f" Package created: {workflow_state['zip_path']}") else: - output_lines.append(" [DRY RUN] Would upload to Claude (if API key set)") + output_lines.append(f" [DRY RUN] Would upload to {adaptor.PLATFORM_NAME} (if API key set)") output_lines.append("") @@ -485,14 +753,22 @@ async def install_skill_tool(args: dict) -> List[TextContent]: output_lines.append(f" Skill package: {workflow_state['zip_path']}") output_lines.append("") - if auto_upload and has_api_key: - output_lines.append("šŸŽ‰ Your skill is now available in Claude!") - output_lines.append(" Go to https://claude.ai/skills to use it") + if auto_upload and has_api_key and target != 'markdown': + # Platform-specific success message + if target == 'claude': + output_lines.append("šŸŽ‰ Your skill is now available in Claude!") + output_lines.append(" Go to https://claude.ai/skills to use it") + elif target == 'gemini': + output_lines.append("šŸŽ‰ Your skill is now available in Gemini!") + output_lines.append(" Go to https://aistudio.google.com/ to use it") + elif target == 'openai': + output_lines.append("šŸŽ‰ Your assistant is now available in OpenAI!") + output_lines.append(" Go to https://platform.openai.com/assistants/ to use it") elif auto_upload: output_lines.append("šŸ“ Manual upload required (see instructions above)") else: output_lines.append("šŸ“¤ To upload:") - output_lines.append(" skill-seekers upload " + workflow_state['zip_path']) + output_lines.append(f" skill-seekers upload {workflow_state['zip_path']} --target {target}") else: output_lines.append("This was a dry run. No actions were taken.") output_lines.append("") diff --git a/src/skill_seekers/mcp/tools/splitting_tools.py b/src/skill_seekers/mcp/tools/splitting_tools.py index 3131846..d8d6e30 100644 --- a/src/skill_seekers/mcp/tools/splitting_tools.py +++ b/src/skill_seekers/mcp/tools/splitting_tools.py @@ -94,17 +94,22 @@ def run_subprocess_with_streaming(cmd, timeout=None): async def split_config(args: dict) -> List[TextContent]: """ - Split large documentation config into multiple focused skills. + Split large configs into multiple focused skills. + + Supports both documentation and unified (multi-source) configs: + - Documentation configs: Split by categories, size, or create router skills + - Unified configs: Split by source type (documentation, github, pdf) For large documentation sites (10K+ pages), this tool splits the config into - multiple smaller configs based on categories, size, or custom strategy. This - improves performance and makes individual skills more focused. + multiple smaller configs. For unified configs with multiple sources, splits + into separate configs per source type. Args: args: Dictionary containing: - - config_path (str): Path to config JSON file (e.g., configs/godot.json) - - strategy (str, optional): Split strategy: auto, none, category, router, size (default: auto) - - target_pages (int, optional): Target pages per skill (default: 5000) + - config_path (str): Path to config JSON file (e.g., configs/godot.json or configs/react_unified.json) + - strategy (str, optional): Split strategy: auto, none, source, category, router, size (default: auto) + 'source' strategy is for unified configs only + - target_pages (int, optional): Target pages per skill for doc configs (default: 5000) - dry_run (bool, optional): Preview without saving files (default: False) Returns: diff --git a/tests/test_install_multiplatform.py b/tests/test_install_multiplatform.py new file mode 100644 index 0000000..5edfe04 --- /dev/null +++ b/tests/test_install_multiplatform.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +""" +Tests for multi-platform install workflow +""" + +import unittest +from unittest.mock import patch, MagicMock, AsyncMock +import asyncio +from pathlib import Path + + +class TestInstallCLI(unittest.TestCase): + """Test install_skill CLI with multi-platform support""" + + def test_cli_accepts_target_flag(self): + """Test that CLI accepts --target flag""" + import argparse + import sys + from pathlib import Path + + # Mock sys.path to import install_skill module + sys.path.insert(0, str(Path(__file__).parent.parent / "src" / "skill_seekers" / "cli")) + + try: + # Create parser like install_skill.py does + parser = argparse.ArgumentParser() + parser.add_argument("--config", required=True) + parser.add_argument("--target", choices=['claude', 'gemini', 'openai', 'markdown'], default='claude') + + # Test that each platform is accepted + for platform in ['claude', 'gemini', 'openai', 'markdown']: + args = parser.parse_args(['--config', 'test', '--target', platform]) + self.assertEqual(args.target, platform) + + # Test default is claude + args = parser.parse_args(['--config', 'test']) + self.assertEqual(args.target, 'claude') + + finally: + sys.path.pop(0) + + def test_cli_rejects_invalid_target(self): + """Test that CLI rejects invalid --target values""" + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument("--config", required=True) + parser.add_argument("--target", choices=['claude', 'gemini', 'openai', 'markdown'], default='claude') + + # Should raise SystemExit for invalid target + with self.assertRaises(SystemExit): + parser.parse_args(['--config', 'test', '--target', 'invalid']) + + +class TestInstallToolMultiPlatform(unittest.IsolatedAsyncioTestCase): + """Test install_skill_tool with multi-platform support""" + + async def test_install_tool_accepts_target_parameter(self): + """Test that install_skill_tool accepts target parameter""" + from skill_seekers.mcp.tools.packaging_tools import install_skill_tool + + # Just test dry_run mode which doesn't need mocking all internal tools + # Test with each platform + for target in ['claude', 'gemini', 'openai']: + # Use dry_run=True which skips actual execution + # It will still show us the platform is being recognized + with patch('builtins.open', create=True) as mock_open, \ + patch('json.load') as mock_json_load: + + # Mock config file reading + mock_json_load.return_value = {'name': 'test-skill'} + mock_file = MagicMock() + mock_file.__enter__ = lambda s: s + mock_file.__exit__ = MagicMock() + mock_open.return_value = mock_file + + result = await install_skill_tool({ + "config_path": "configs/test.json", + "target": target, + "dry_run": True + }) + + # Verify result mentions the correct platform + result_text = result[0].text + self.assertIsInstance(result_text, str) + self.assertIn("WORKFLOW COMPLETE", result_text) + + async def test_install_tool_uses_correct_adaptor(self): + """Test that install_skill_tool uses the correct adaptor for each platform""" + from skill_seekers.mcp.tools.packaging_tools import install_skill_tool + from skill_seekers.cli.adaptors import get_adaptor + + # Test that each platform creates the right adaptor + for target in ['claude', 'gemini', 'openai', 'markdown']: + adaptor = get_adaptor(target) + self.assertEqual(adaptor.PLATFORM, target) + + async def test_install_tool_platform_specific_api_keys(self): + """Test that install_tool checks for correct API key per platform""" + from skill_seekers.cli.adaptors import get_adaptor + + # Test API key env var names + claude_adaptor = get_adaptor('claude') + self.assertEqual(claude_adaptor.get_env_var_name(), 'ANTHROPIC_API_KEY') + + gemini_adaptor = get_adaptor('gemini') + self.assertEqual(gemini_adaptor.get_env_var_name(), 'GOOGLE_API_KEY') + + openai_adaptor = get_adaptor('openai') + self.assertEqual(openai_adaptor.get_env_var_name(), 'OPENAI_API_KEY') + + markdown_adaptor = get_adaptor('markdown') + # Markdown doesn't need an API key, but should still have a method + self.assertIsNotNone(markdown_adaptor.get_env_var_name()) + + +class TestInstallWorkflowIntegration(unittest.IsolatedAsyncioTestCase): + """Integration tests for full install workflow""" + + async def test_dry_run_shows_correct_platform(self): + """Test dry run shows correct platform in output""" + from skill_seekers.cli.adaptors import get_adaptor + + # Test each platform shows correct platform name + platforms = { + 'claude': 'Claude AI (Anthropic)', + 'gemini': 'Google Gemini', + 'openai': 'OpenAI ChatGPT', + 'markdown': 'Generic Markdown (Universal)' + } + + for target, expected_name in platforms.items(): + adaptor = get_adaptor(target) + self.assertEqual(adaptor.PLATFORM_NAME, expected_name) + + +if __name__ == '__main__': + unittest.main()