Features: - New upload_skill.py for automatic API-based upload - Smart detection: upload if API key available, helpful message if not - Enhanced package_skill.py with --upload flag - New MCP tool: upload_skill (9 total MCP tools now) - Enhanced MCP tool: package_skill with smart auto-upload - Cross-platform folder opening in utils.py - Graceful error handling throughout Fixes: - Fix missing import os in mcp/server.py - Fix package_skill.py exit code (now 0 when API key missing) - Improve UX with helpful messages instead of errors Tests: 14/14 passed (100%) - CLI tests: 8/8 passed - MCP tests: 6/6 passed Files: +4 new, 5 modified, ~600 lines added
173 lines
4.3 KiB
Python
Executable File
173 lines
4.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Utility functions for Skill Seeker CLI tools
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import subprocess
|
|
import platform
|
|
from pathlib import Path
|
|
|
|
|
|
def open_folder(folder_path):
|
|
"""
|
|
Open a folder in the system file browser
|
|
|
|
Args:
|
|
folder_path: Path to folder to open
|
|
|
|
Returns:
|
|
bool: True if successful, False otherwise
|
|
"""
|
|
folder_path = Path(folder_path).resolve()
|
|
|
|
if not folder_path.exists():
|
|
print(f"⚠️ Folder not found: {folder_path}")
|
|
return False
|
|
|
|
system = platform.system()
|
|
|
|
try:
|
|
if system == "Linux":
|
|
# Try xdg-open first (standard)
|
|
subprocess.run(["xdg-open", str(folder_path)], check=True)
|
|
elif system == "Darwin": # macOS
|
|
subprocess.run(["open", str(folder_path)], check=True)
|
|
elif system == "Windows":
|
|
subprocess.run(["explorer", str(folder_path)], check=True)
|
|
else:
|
|
print(f"⚠️ Unknown operating system: {system}")
|
|
return False
|
|
|
|
return True
|
|
|
|
except subprocess.CalledProcessError:
|
|
print(f"⚠️ Could not open folder automatically")
|
|
return False
|
|
except FileNotFoundError:
|
|
print(f"⚠️ File browser not found on system")
|
|
return False
|
|
|
|
|
|
def has_api_key():
|
|
"""
|
|
Check if ANTHROPIC_API_KEY is set in environment
|
|
|
|
Returns:
|
|
bool: True if API key is set, False otherwise
|
|
"""
|
|
api_key = os.environ.get('ANTHROPIC_API_KEY', '').strip()
|
|
return len(api_key) > 0
|
|
|
|
|
|
def get_api_key():
|
|
"""
|
|
Get ANTHROPIC_API_KEY from environment
|
|
|
|
Returns:
|
|
str: API key or None if not set
|
|
"""
|
|
api_key = os.environ.get('ANTHROPIC_API_KEY', '').strip()
|
|
return api_key if api_key else None
|
|
|
|
|
|
def get_upload_url():
|
|
"""
|
|
Get the Claude skills upload URL
|
|
|
|
Returns:
|
|
str: Claude skills upload URL
|
|
"""
|
|
return "https://claude.ai/skills"
|
|
|
|
|
|
def print_upload_instructions(zip_path):
|
|
"""
|
|
Print clear upload instructions for manual upload
|
|
|
|
Args:
|
|
zip_path: Path to the .zip file to upload
|
|
"""
|
|
zip_path = Path(zip_path)
|
|
|
|
print()
|
|
print("╔══════════════════════════════════════════════════════════╗")
|
|
print("║ NEXT STEP ║")
|
|
print("╚══════════════════════════════════════════════════════════╝")
|
|
print()
|
|
print(f"📤 Upload to Claude: {get_upload_url()}")
|
|
print()
|
|
print(f"1. Go to {get_upload_url()}")
|
|
print("2. Click \"Upload Skill\"")
|
|
print(f"3. Select: {zip_path}")
|
|
print("4. Done! ✅")
|
|
print()
|
|
|
|
|
|
def format_file_size(size_bytes):
|
|
"""
|
|
Format file size in human-readable format
|
|
|
|
Args:
|
|
size_bytes: Size in bytes
|
|
|
|
Returns:
|
|
str: Formatted size (e.g., "45.3 KB")
|
|
"""
|
|
if size_bytes < 1024:
|
|
return f"{size_bytes} bytes"
|
|
elif size_bytes < 1024 * 1024:
|
|
return f"{size_bytes / 1024:.1f} KB"
|
|
else:
|
|
return f"{size_bytes / (1024 * 1024):.1f} MB"
|
|
|
|
|
|
def validate_skill_directory(skill_dir):
|
|
"""
|
|
Validate that a directory is a valid skill directory
|
|
|
|
Args:
|
|
skill_dir: Path to skill directory
|
|
|
|
Returns:
|
|
tuple: (is_valid, error_message)
|
|
"""
|
|
skill_path = Path(skill_dir)
|
|
|
|
if not skill_path.exists():
|
|
return False, f"Directory not found: {skill_dir}"
|
|
|
|
if not skill_path.is_dir():
|
|
return False, f"Not a directory: {skill_dir}"
|
|
|
|
skill_md = skill_path / "SKILL.md"
|
|
if not skill_md.exists():
|
|
return False, f"SKILL.md not found in {skill_dir}"
|
|
|
|
return True, None
|
|
|
|
|
|
def validate_zip_file(zip_path):
|
|
"""
|
|
Validate that a file is a valid skill .zip file
|
|
|
|
Args:
|
|
zip_path: Path to .zip file
|
|
|
|
Returns:
|
|
tuple: (is_valid, error_message)
|
|
"""
|
|
zip_path = Path(zip_path)
|
|
|
|
if not zip_path.exists():
|
|
return False, f"File not found: {zip_path}"
|
|
|
|
if not zip_path.is_file():
|
|
return False, f"Not a file: {zip_path}"
|
|
|
|
if not zip_path.suffix == '.zip':
|
|
return False, f"Not a .zip file: {zip_path}"
|
|
|
|
return True, None
|