- Fix #299: rename --chunk-size/--chunk-overlap to --streaming-chunk-size/ --streaming-overlap in arguments/package.py to avoid collision with the RAG --chunk-size flag from arguments/common.py - Phase 1a: make package_skill.py import args via add_package_arguments() instead of a 105-line inline duplicate argparse block; fixes the root cause of _reconstruct_argv() passing unrecognised flag names - Phase 1b: centralise setup_logging() into utils.py and remove 4 duplicate module-level logging.basicConfig() calls from doc_scraper.py, github_scraper.py, codebase_scraper.py, and unified_scraper.py - Fix test_package_structure.py / test_cli_paths.py version strings (3.1.1 → 3.1.2) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
296 lines
9.2 KiB
Python
296 lines
9.2 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Simple Skill Packager
|
||
Packages a skill directory into a .zip file for Claude.
|
||
|
||
Usage:
|
||
skill-seekers package output/steam-inventory/
|
||
skill-seekers package output/react/
|
||
skill-seekers package output/react/ --no-open # Don't open folder
|
||
"""
|
||
|
||
import argparse
|
||
import os
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
# Import utilities
|
||
try:
|
||
from quality_checker import SkillQualityChecker, print_report
|
||
from utils import (
|
||
format_file_size,
|
||
open_folder,
|
||
print_upload_instructions,
|
||
validate_skill_directory,
|
||
)
|
||
except ImportError:
|
||
# If running from different directory, add cli to path
|
||
sys.path.insert(0, str(Path(__file__).parent))
|
||
from quality_checker import SkillQualityChecker, print_report
|
||
from utils import (
|
||
format_file_size,
|
||
open_folder,
|
||
print_upload_instructions,
|
||
validate_skill_directory,
|
||
)
|
||
|
||
|
||
def package_skill(
|
||
skill_dir,
|
||
open_folder_after=True,
|
||
skip_quality_check=False,
|
||
target="claude",
|
||
streaming=False,
|
||
chunk_size=4000,
|
||
chunk_overlap=200,
|
||
batch_size=100,
|
||
enable_chunking=False,
|
||
chunk_max_tokens=512,
|
||
preserve_code_blocks=True,
|
||
):
|
||
"""
|
||
Package a skill directory into platform-specific format
|
||
|
||
Args:
|
||
skill_dir: Path to skill directory
|
||
open_folder_after: Whether to open the output folder after packaging
|
||
skip_quality_check: Skip quality checks before packaging
|
||
target: Target LLM platform ('claude', 'gemini', 'openai', 'markdown')
|
||
streaming: Use streaming ingestion for large docs
|
||
chunk_size: Maximum characters per chunk (streaming mode)
|
||
chunk_overlap: Overlap between chunks (streaming mode)
|
||
batch_size: Number of chunks per batch (streaming mode)
|
||
enable_chunking: Enable intelligent chunking for RAG platforms
|
||
chunk_max_tokens: Maximum tokens per chunk (default: 512)
|
||
preserve_code_blocks: Preserve code blocks during chunking
|
||
|
||
Returns:
|
||
tuple: (success, package_path) where success is bool and package_path is Path or None
|
||
"""
|
||
skill_path = Path(skill_dir)
|
||
|
||
# Validate skill directory
|
||
is_valid, error_msg = validate_skill_directory(skill_path)
|
||
if not is_valid:
|
||
print(f"❌ Error: {error_msg}")
|
||
return False, None
|
||
|
||
# Run quality checks (unless skipped)
|
||
if not skip_quality_check:
|
||
print("\n" + "=" * 60)
|
||
print("QUALITY CHECK")
|
||
print("=" * 60)
|
||
|
||
checker = SkillQualityChecker(skill_path)
|
||
report = checker.check_all()
|
||
|
||
# Print report
|
||
print_report(report, verbose=False)
|
||
|
||
# If there are errors or warnings, ask user to confirm
|
||
if report.has_errors or report.has_warnings:
|
||
print("=" * 60)
|
||
response = input("\nContinue with packaging? (y/n): ").strip().lower()
|
||
if response != "y":
|
||
print("\n❌ Packaging cancelled by user")
|
||
return False, None
|
||
print()
|
||
else:
|
||
print("=" * 60)
|
||
print()
|
||
|
||
# Get platform-specific adaptor
|
||
try:
|
||
from skill_seekers.cli.adaptors import get_adaptor
|
||
|
||
adaptor = get_adaptor(target)
|
||
except (ImportError, ValueError) as e:
|
||
print(f"❌ Error: {e}")
|
||
return False, None
|
||
|
||
# Create package using adaptor
|
||
skill_name = skill_path.name
|
||
output_dir = skill_path.parent
|
||
|
||
# Auto-enable chunking for RAG platforms
|
||
RAG_PLATFORMS = [
|
||
"langchain",
|
||
"llama-index",
|
||
"haystack",
|
||
"weaviate",
|
||
"chroma",
|
||
"faiss",
|
||
"qdrant",
|
||
]
|
||
|
||
if target in RAG_PLATFORMS and not enable_chunking:
|
||
print(f"ℹ️ Auto-enabling chunking for {target} platform")
|
||
enable_chunking = True
|
||
|
||
print(f"📦 Packaging skill: {skill_name}")
|
||
print(f" Target: {adaptor.PLATFORM_NAME}")
|
||
print(f" Source: {skill_path}")
|
||
|
||
if streaming:
|
||
print(f" Mode: Streaming (chunk_size={chunk_size}, overlap={chunk_overlap})")
|
||
elif enable_chunking:
|
||
print(
|
||
f" Chunking: Enabled (max_tokens={chunk_max_tokens}, preserve_code={preserve_code_blocks})"
|
||
)
|
||
|
||
try:
|
||
# Use streaming if requested and supported
|
||
if streaming and hasattr(adaptor, "package_streaming"):
|
||
package_path = adaptor.package_streaming(
|
||
skill_path,
|
||
output_dir,
|
||
chunk_size=chunk_size,
|
||
chunk_overlap=chunk_overlap,
|
||
batch_size=batch_size,
|
||
)
|
||
elif streaming:
|
||
print("⚠️ Streaming not supported for this platform, using standard packaging")
|
||
package_path = adaptor.package(
|
||
skill_path,
|
||
output_dir,
|
||
enable_chunking=enable_chunking,
|
||
chunk_max_tokens=chunk_max_tokens,
|
||
preserve_code_blocks=preserve_code_blocks,
|
||
)
|
||
else:
|
||
package_path = adaptor.package(
|
||
skill_path,
|
||
output_dir,
|
||
enable_chunking=enable_chunking,
|
||
chunk_max_tokens=chunk_max_tokens,
|
||
preserve_code_blocks=preserve_code_blocks,
|
||
)
|
||
|
||
print(f" Output: {package_path}")
|
||
except Exception as e:
|
||
print(f"❌ Error creating package: {e}")
|
||
return False, None
|
||
|
||
# Get package size
|
||
package_size = package_path.stat().st_size
|
||
print(f"\n✅ Package created: {package_path}")
|
||
print(f" Size: {package_size:,} bytes ({format_file_size(package_size)})")
|
||
|
||
# Open folder in file browser
|
||
if open_folder_after:
|
||
print(f"\n📂 Opening folder: {package_path.parent}")
|
||
open_folder(package_path.parent)
|
||
|
||
# Print upload instructions
|
||
print_upload_instructions(package_path)
|
||
|
||
return True, package_path
|
||
|
||
|
||
def main():
|
||
from skill_seekers.cli.arguments.package import add_package_arguments
|
||
|
||
parser = argparse.ArgumentParser(
|
||
description="Package a skill directory into a .zip file for Claude",
|
||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||
epilog="""
|
||
Examples:
|
||
# Package skill with quality checks (recommended)
|
||
skill-seekers package output/react/
|
||
|
||
# Package skill without opening folder
|
||
skill-seekers package output/react/ --no-open
|
||
|
||
# Skip quality checks (faster, but not recommended)
|
||
skill-seekers package output/react/ --skip-quality-check
|
||
|
||
# Package and auto-upload to Claude
|
||
skill-seekers package output/react/ --upload
|
||
|
||
# Get help
|
||
skill-seekers package --help
|
||
""",
|
||
)
|
||
|
||
add_package_arguments(parser)
|
||
args = parser.parse_args()
|
||
|
||
success, package_path = package_skill(
|
||
args.skill_directory,
|
||
open_folder_after=not args.no_open,
|
||
skip_quality_check=args.skip_quality_check,
|
||
target=args.target,
|
||
streaming=args.streaming,
|
||
chunk_size=args.streaming_chunk_size,
|
||
chunk_overlap=args.streaming_overlap,
|
||
batch_size=args.batch_size,
|
||
enable_chunking=args.chunk,
|
||
chunk_max_tokens=args.chunk_tokens,
|
||
preserve_code_blocks=not args.no_preserve_code,
|
||
)
|
||
|
||
if not success:
|
||
sys.exit(1)
|
||
|
||
# Auto-upload if requested
|
||
if args.upload:
|
||
try:
|
||
from skill_seekers.cli.adaptors import get_adaptor
|
||
|
||
# Get adaptor for target platform
|
||
adaptor = get_adaptor(args.target)
|
||
|
||
# Get API key from environment
|
||
api_key = os.environ.get(adaptor.get_env_var_name(), "").strip()
|
||
|
||
if not api_key:
|
||
# No API key - show helpful message but DON'T fail
|
||
print("\n" + "=" * 60)
|
||
print("💡 Automatic Upload")
|
||
print("=" * 60)
|
||
print()
|
||
print(f"To enable automatic upload to {adaptor.PLATFORM_NAME}:")
|
||
print(" 1. Get API key from the platform")
|
||
print(f" 2. Set: export {adaptor.get_env_var_name()}=...")
|
||
print(" 3. Run package command with --upload flag")
|
||
print()
|
||
print("For now, use manual upload (instructions above) ☝️")
|
||
print("=" * 60)
|
||
# Exit successfully - packaging worked!
|
||
sys.exit(0)
|
||
|
||
# API key exists - try upload
|
||
print("\n" + "=" * 60)
|
||
print(f"📤 Uploading to {adaptor.PLATFORM_NAME}...")
|
||
print("=" * 60)
|
||
|
||
result = adaptor.upload(package_path, api_key)
|
||
|
||
if result["success"]:
|
||
print(f"\n✅ {result['message']}")
|
||
if result["url"]:
|
||
print(f" View at: {result['url']}")
|
||
print("=" * 60)
|
||
sys.exit(0)
|
||
else:
|
||
print(f"\n❌ Upload failed: {result['message']}")
|
||
print()
|
||
print("💡 Try manual upload instead (instructions above) ☝️")
|
||
print("=" * 60)
|
||
# Exit successfully - packaging worked even if upload failed
|
||
sys.exit(0)
|
||
|
||
except ImportError as e:
|
||
print(f"\n❌ Error: {e}")
|
||
print("Install required dependencies for this platform")
|
||
sys.exit(1)
|
||
except Exception as e:
|
||
print(f"\n❌ Upload error: {e}")
|
||
sys.exit(1)
|
||
|
||
sys.exit(0)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|