diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5b532c0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,149 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "skill-seekers" +version = "2.0.0" +description = "Convert documentation websites, GitHub repositories, and PDFs into Claude AI skills" +readme = "README.md" +requires-python = ">=3.10" +license = {text = "MIT"} +authors = [ + {name = "Yusuf Karaaslan"} +] +keywords = [ + "claude", + "ai", + "documentation", + "scraping", + "skills", + "llm", + "mcp", + "automation" +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Software Development :: Documentation", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Text Processing :: Markup :: Markdown", +] + +# Core dependencies +dependencies = [ + "requests>=2.32.5", + "beautifulsoup4>=4.14.2", + "PyGithub>=2.5.0", + "mcp>=1.18.0", + "httpx>=0.28.1", + "httpx-sse>=0.4.3", + "PyMuPDF>=1.24.14", + "Pillow>=11.0.0", + "pytesseract>=0.3.13", + "pydantic>=2.12.3", + "pydantic-settings>=2.11.0", + "python-dotenv>=1.1.1", + "jsonschema>=4.25.1", + "click>=8.3.0", + "Pygments>=2.19.2", +] + +[project.optional-dependencies] +# Development dependencies +dev = [ + "pytest>=8.4.2", + "pytest-cov>=7.0.0", + "coverage>=7.11.0", +] + +# MCP server dependencies (included by default, but optional) +mcp = [ + "mcp>=1.18.0", + "httpx>=0.28.1", + "httpx-sse>=0.4.3", + "uvicorn>=0.38.0", + "starlette>=0.48.0", + "sse-starlette>=3.0.2", +] + +# All optional dependencies combined +all = [ + "pytest>=8.4.2", + "pytest-cov>=7.0.0", + "coverage>=7.11.0", + "mcp>=1.18.0", + "httpx>=0.28.1", + "httpx-sse>=0.4.3", + "uvicorn>=0.38.0", + "starlette>=0.48.0", + "sse-starlette>=3.0.2", +] + +[project.urls] +Homepage = "https://github.com/yusufkaraaslan/Skill_Seekers" +Repository = "https://github.com/yusufkaraaslan/Skill_Seekers" +"Bug Tracker" = "https://github.com/yusufkaraaslan/Skill_Seekers/issues" +Documentation = "https://github.com/yusufkaraaslan/Skill_Seekers#readme" + +[project.scripts] +# Main unified CLI +skill-seekers = "skill_seekers.cli.main:main" + +# Individual tool entry points +skill-seekers-scrape = "skill_seekers.cli.doc_scraper:main" +skill-seekers-github = "skill_seekers.cli.github_scraper:main" +skill-seekers-pdf = "skill_seekers.cli.pdf_scraper:main" +skill-seekers-unified = "skill_seekers.cli.unified_scraper:main" +skill-seekers-enhance = "skill_seekers.cli.enhance_skill_local:main" +skill-seekers-package = "skill_seekers.cli.package_skill:main" +skill-seekers-upload = "skill_seekers.cli.upload_skill:main" +skill-seekers-estimate = "skill_seekers.cli.estimate_pages:main" + +[tool.setuptools] +packages = ["skill_seekers", "skill_seekers.cli", "skill_seekers.mcp", "skill_seekers.mcp.tools"] + +[tool.setuptools.package-dir] +"" = "src" + +[tool.setuptools.package-data] +skill_seekers = ["py.typed"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = "-v --tb=short --strict-markers" + +[tool.coverage.run] +source = ["src/skill_seekers"] +omit = ["*/tests/*", "*/__pycache__/*", "*/venv/*"] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "raise AssertionError", + "raise NotImplementedError", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", + "@abstractmethod", +] + +[tool.uv] +dev-dependencies = [ + "pytest>=8.4.2", + "pytest-cov>=7.0.0", + "coverage>=7.11.0", +] + +[tool.uv.sources] +# Use PyPI for all dependencies diff --git a/src/skill_seekers/__init__.py b/src/skill_seekers/__init__.py new file mode 100644 index 0000000..752904b --- /dev/null +++ b/src/skill_seekers/__init__.py @@ -0,0 +1,22 @@ +""" +Skill Seekers - Convert documentation, GitHub repos, and PDFs into Claude AI skills. + +This package provides tools for automatically scraping, organizing, and packaging +documentation from various sources into uploadable Claude AI skills. +""" + +__version__ = "2.0.0" +__author__ = "Yusuf Karaaslan" +__license__ = "MIT" + +# Expose main components for easier imports +from skill_seekers.cli import __version__ as cli_version +from skill_seekers.mcp import __version__ as mcp_version + +__all__ = [ + "__version__", + "__author__", + "__license__", + "cli_version", + "mcp_version", +] diff --git a/cli/__init__.py b/src/skill_seekers/cli/__init__.py similarity index 98% rename from cli/__init__.py rename to src/skill_seekers/cli/__init__.py index 0a6b120..d782d5d 100644 --- a/cli/__init__.py +++ b/src/skill_seekers/cli/__init__.py @@ -28,7 +28,7 @@ except ImportError: open_folder = None read_reference_files = None -__version__ = "1.3.0" +__version__ = "2.0.0" __all__ = [ "LlmsTxtDetector", diff --git a/cli/code_analyzer.py b/src/skill_seekers/cli/code_analyzer.py similarity index 100% rename from cli/code_analyzer.py rename to src/skill_seekers/cli/code_analyzer.py diff --git a/cli/config_validator.py b/src/skill_seekers/cli/config_validator.py similarity index 100% rename from cli/config_validator.py rename to src/skill_seekers/cli/config_validator.py diff --git a/cli/conflict_detector.py b/src/skill_seekers/cli/conflict_detector.py similarity index 100% rename from cli/conflict_detector.py rename to src/skill_seekers/cli/conflict_detector.py diff --git a/cli/constants.py b/src/skill_seekers/cli/constants.py similarity index 100% rename from cli/constants.py rename to src/skill_seekers/cli/constants.py diff --git a/cli/doc_scraper.py b/src/skill_seekers/cli/doc_scraper.py similarity index 99% rename from cli/doc_scraper.py rename to src/skill_seekers/cli/doc_scraper.py index a7125be..2654163 100755 --- a/cli/doc_scraper.py +++ b/src/skill_seekers/cli/doc_scraper.py @@ -29,10 +29,10 @@ from typing import Optional, Dict, List, Tuple, Set, Deque, Any # Add parent directory to path for imports when run as script sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from cli.llms_txt_detector import LlmsTxtDetector -from cli.llms_txt_parser import LlmsTxtParser -from cli.llms_txt_downloader import LlmsTxtDownloader -from cli.constants import ( +from skill_seekers.cli.llms_txt_detector import LlmsTxtDetector +from skill_seekers.cli.llms_txt_parser import LlmsTxtParser +from skill_seekers.cli.llms_txt_downloader import LlmsTxtDownloader +from skill_seekers.cli.constants import ( DEFAULT_RATE_LIMIT, DEFAULT_MAX_PAGES, DEFAULT_CHECKPOINT_INTERVAL, diff --git a/cli/enhance_skill.py b/src/skill_seekers/cli/enhance_skill.py similarity index 98% rename from cli/enhance_skill.py rename to src/skill_seekers/cli/enhance_skill.py index a758825..fbe7827 100644 --- a/cli/enhance_skill.py +++ b/src/skill_seekers/cli/enhance_skill.py @@ -18,8 +18,8 @@ from pathlib import Path # Add parent directory to path for imports when run as script sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from cli.constants import API_CONTENT_LIMIT, API_PREVIEW_LIMIT -from cli.utils import read_reference_files +from skill_seekers.cli.constants import API_CONTENT_LIMIT, API_PREVIEW_LIMIT +from skill_seekers.cli.utils import read_reference_files try: import anthropic diff --git a/cli/enhance_skill_local.py b/src/skill_seekers/cli/enhance_skill_local.py similarity index 98% rename from cli/enhance_skill_local.py rename to src/skill_seekers/cli/enhance_skill_local.py index 49d7f77..ee4801c 100644 --- a/cli/enhance_skill_local.py +++ b/src/skill_seekers/cli/enhance_skill_local.py @@ -28,8 +28,8 @@ from pathlib import Path # Add parent directory to path for imports when run as script sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from cli.constants import LOCAL_CONTENT_LIMIT, LOCAL_PREVIEW_LIMIT -from cli.utils import read_reference_files +from skill_seekers.cli.constants import LOCAL_CONTENT_LIMIT, LOCAL_PREVIEW_LIMIT +from skill_seekers.cli.utils import read_reference_files def detect_terminal_app(): diff --git a/cli/estimate_pages.py b/src/skill_seekers/cli/estimate_pages.py similarity index 99% rename from cli/estimate_pages.py rename to src/skill_seekers/cli/estimate_pages.py index 4fb6607..0678421 100755 --- a/cli/estimate_pages.py +++ b/src/skill_seekers/cli/estimate_pages.py @@ -15,7 +15,7 @@ import json # Add parent directory to path for imports when run as script sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from cli.constants import ( +from skill_seekers.cli.constants import ( DEFAULT_RATE_LIMIT, DEFAULT_MAX_DISCOVERY, DISCOVERY_THRESHOLD diff --git a/cli/generate_router.py b/src/skill_seekers/cli/generate_router.py similarity index 100% rename from cli/generate_router.py rename to src/skill_seekers/cli/generate_router.py diff --git a/cli/github_scraper.py b/src/skill_seekers/cli/github_scraper.py similarity index 100% rename from cli/github_scraper.py rename to src/skill_seekers/cli/github_scraper.py diff --git a/cli/llms_txt_detector.py b/src/skill_seekers/cli/llms_txt_detector.py similarity index 100% rename from cli/llms_txt_detector.py rename to src/skill_seekers/cli/llms_txt_detector.py diff --git a/cli/llms_txt_downloader.py b/src/skill_seekers/cli/llms_txt_downloader.py similarity index 100% rename from cli/llms_txt_downloader.py rename to src/skill_seekers/cli/llms_txt_downloader.py diff --git a/cli/llms_txt_parser.py b/src/skill_seekers/cli/llms_txt_parser.py similarity index 100% rename from cli/llms_txt_parser.py rename to src/skill_seekers/cli/llms_txt_parser.py diff --git a/src/skill_seekers/cli/main.py b/src/skill_seekers/cli/main.py new file mode 100644 index 0000000..a843b97 --- /dev/null +++ b/src/skill_seekers/cli/main.py @@ -0,0 +1,285 @@ +#!/usr/bin/env python3 +""" +Skill Seekers - Unified CLI Entry Point + +Provides a git-style unified command-line interface for all Skill Seekers tools. + +Usage: + skill-seekers [options] + +Commands: + scrape Scrape documentation website + github Scrape GitHub repository + pdf Extract from PDF file + unified Multi-source scraping (docs + GitHub + PDF) + enhance AI-powered enhancement (local, no API key) + package Package skill into .zip file + upload Upload skill to Claude + estimate Estimate page count before scraping + +Examples: + skill-seekers scrape --config configs/react.json + skill-seekers github --repo microsoft/TypeScript + skill-seekers unified --config configs/react_unified.json + skill-seekers package output/react/ +""" + +import sys +import argparse +from typing import List, Optional + + +def create_parser() -> argparse.ArgumentParser: + """Create the main argument parser with subcommands.""" + parser = argparse.ArgumentParser( + prog="skill-seekers", + description="Convert documentation, GitHub repos, and PDFs into Claude AI skills", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Scrape documentation + skill-seekers scrape --config configs/react.json + + # Scrape GitHub repository + skill-seekers github --repo microsoft/TypeScript --name typescript + + # Multi-source scraping (unified) + skill-seekers unified --config configs/react_unified.json + + # AI-powered enhancement + skill-seekers enhance output/react/ + + # Package and upload + skill-seekers package output/react/ + skill-seekers upload output/react.zip + +For more information: https://github.com/yusufkaraaslan/Skill_Seekers + """ + ) + + parser.add_argument( + "--version", + action="version", + version="%(prog)s 2.0.0" + ) + + subparsers = parser.add_subparsers( + dest="command", + title="commands", + description="Available Skill Seekers commands", + help="Command to run" + ) + + # === scrape subcommand === + scrape_parser = subparsers.add_parser( + "scrape", + help="Scrape documentation website", + description="Scrape documentation website and generate skill" + ) + scrape_parser.add_argument("--config", help="Config JSON file") + scrape_parser.add_argument("--name", help="Skill name") + scrape_parser.add_argument("--url", help="Documentation URL") + scrape_parser.add_argument("--description", help="Skill description") + scrape_parser.add_argument("--skip-scrape", action="store_true", help="Skip scraping, use cached data") + scrape_parser.add_argument("--enhance", action="store_true", help="AI enhancement (API)") + scrape_parser.add_argument("--enhance-local", action="store_true", help="AI enhancement (local)") + scrape_parser.add_argument("--dry-run", action="store_true", help="Dry run mode") + scrape_parser.add_argument("--async", dest="async_mode", action="store_true", help="Use async scraping") + scrape_parser.add_argument("--workers", type=int, help="Number of async workers") + + # === github subcommand === + github_parser = subparsers.add_parser( + "github", + help="Scrape GitHub repository", + description="Scrape GitHub repository and generate skill" + ) + github_parser.add_argument("--config", help="Config JSON file") + github_parser.add_argument("--repo", help="GitHub repo (owner/repo)") + github_parser.add_argument("--name", help="Skill name") + github_parser.add_argument("--description", help="Skill description") + + # === pdf subcommand === + pdf_parser = subparsers.add_parser( + "pdf", + help="Extract from PDF file", + description="Extract content from PDF and generate skill" + ) + pdf_parser.add_argument("--config", help="Config JSON file") + pdf_parser.add_argument("--pdf", help="PDF file path") + pdf_parser.add_argument("--name", help="Skill name") + pdf_parser.add_argument("--description", help="Skill description") + pdf_parser.add_argument("--from-json", help="Build from extracted JSON") + + # === unified subcommand === + unified_parser = subparsers.add_parser( + "unified", + help="Multi-source scraping (docs + GitHub + PDF)", + description="Combine multiple sources into one skill" + ) + unified_parser.add_argument("--config", required=True, help="Unified config JSON file") + unified_parser.add_argument("--merge-mode", help="Merge mode (rule-based, claude-enhanced)") + unified_parser.add_argument("--dry-run", action="store_true", help="Dry run mode") + + # === enhance subcommand === + enhance_parser = subparsers.add_parser( + "enhance", + help="AI-powered enhancement (local, no API key)", + description="Enhance SKILL.md using Claude Code (local)" + ) + enhance_parser.add_argument("skill_directory", help="Skill directory path") + + # === package subcommand === + package_parser = subparsers.add_parser( + "package", + help="Package skill into .zip file", + description="Package skill directory into uploadable .zip" + ) + package_parser.add_argument("skill_directory", help="Skill directory path") + package_parser.add_argument("--no-open", action="store_true", help="Don't open output folder") + package_parser.add_argument("--upload", action="store_true", help="Auto-upload after packaging") + + # === upload subcommand === + upload_parser = subparsers.add_parser( + "upload", + help="Upload skill to Claude", + description="Upload .zip file to Claude via Anthropic API" + ) + upload_parser.add_argument("zip_file", help=".zip file to upload") + upload_parser.add_argument("--api-key", help="Anthropic API key") + + # === estimate subcommand === + estimate_parser = subparsers.add_parser( + "estimate", + help="Estimate page count before scraping", + description="Estimate total pages for documentation scraping" + ) + estimate_parser.add_argument("config", help="Config JSON file") + estimate_parser.add_argument("--max-discovery", type=int, help="Max pages to discover") + + return parser + + +def main(argv: Optional[List[str]] = None) -> int: + """Main entry point for the unified CLI. + + Args: + argv: Command-line arguments (defaults to sys.argv) + + Returns: + Exit code (0 for success, non-zero for error) + """ + parser = create_parser() + args = parser.parse_args(argv) + + if not args.command: + parser.print_help() + return 1 + + # Delegate to the appropriate tool + try: + if args.command == "scrape": + from skill_seekers.cli.doc_scraper import main as scrape_main + # Convert args namespace to sys.argv format for doc_scraper + sys.argv = ["doc_scraper.py"] + if args.config: + sys.argv.extend(["--config", args.config]) + if args.name: + sys.argv.extend(["--name", args.name]) + if args.url: + sys.argv.extend(["--url", args.url]) + if args.description: + sys.argv.extend(["--description", args.description]) + if args.skip_scrape: + sys.argv.append("--skip-scrape") + if args.enhance: + sys.argv.append("--enhance") + if args.enhance_local: + sys.argv.append("--enhance-local") + if args.dry_run: + sys.argv.append("--dry-run") + if args.async_mode: + sys.argv.append("--async") + if args.workers: + sys.argv.extend(["--workers", str(args.workers)]) + return scrape_main() or 0 + + elif args.command == "github": + from skill_seekers.cli.github_scraper import main as github_main + sys.argv = ["github_scraper.py"] + if args.config: + sys.argv.extend(["--config", args.config]) + if args.repo: + sys.argv.extend(["--repo", args.repo]) + if args.name: + sys.argv.extend(["--name", args.name]) + if args.description: + sys.argv.extend(["--description", args.description]) + return github_main() or 0 + + elif args.command == "pdf": + from skill_seekers.cli.pdf_scraper import main as pdf_main + sys.argv = ["pdf_scraper.py"] + if args.config: + sys.argv.extend(["--config", args.config]) + if args.pdf: + sys.argv.extend(["--pdf", args.pdf]) + if args.name: + sys.argv.extend(["--name", args.name]) + if args.description: + sys.argv.extend(["--description", args.description]) + if args.from_json: + sys.argv.extend(["--from-json", args.from_json]) + return pdf_main() or 0 + + elif args.command == "unified": + from skill_seekers.cli.unified_scraper import main as unified_main + sys.argv = ["unified_scraper.py", "--config", args.config] + if args.merge_mode: + sys.argv.extend(["--merge-mode", args.merge_mode]) + if args.dry_run: + sys.argv.append("--dry-run") + return unified_main() or 0 + + elif args.command == "enhance": + from skill_seekers.cli.enhance_skill_local import main as enhance_main + sys.argv = ["enhance_skill_local.py", args.skill_directory] + return enhance_main() or 0 + + elif args.command == "package": + from skill_seekers.cli.package_skill import main as package_main + sys.argv = ["package_skill.py", args.skill_directory] + if args.no_open: + sys.argv.append("--no-open") + if args.upload: + sys.argv.append("--upload") + return package_main() or 0 + + elif args.command == "upload": + from skill_seekers.cli.upload_skill import main as upload_main + sys.argv = ["upload_skill.py", args.zip_file] + if args.api_key: + sys.argv.extend(["--api-key", args.api_key]) + return upload_main() or 0 + + elif args.command == "estimate": + from skill_seekers.cli.estimate_pages import main as estimate_main + sys.argv = ["estimate_pages.py", args.config] + if args.max_discovery: + sys.argv.extend(["--max-discovery", str(args.max_discovery)]) + return estimate_main() or 0 + + else: + print(f"Error: Unknown command '{args.command}'", file=sys.stderr) + parser.print_help() + return 1 + + except KeyboardInterrupt: + print("\n\nInterrupted by user", file=sys.stderr) + return 130 + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/cli/merge_sources.py b/src/skill_seekers/cli/merge_sources.py similarity index 100% rename from cli/merge_sources.py rename to src/skill_seekers/cli/merge_sources.py diff --git a/cli/package_multi.py b/src/skill_seekers/cli/package_multi.py similarity index 100% rename from cli/package_multi.py rename to src/skill_seekers/cli/package_multi.py diff --git a/cli/package_skill.py b/src/skill_seekers/cli/package_skill.py similarity index 100% rename from cli/package_skill.py rename to src/skill_seekers/cli/package_skill.py diff --git a/cli/pdf_extractor_poc.py b/src/skill_seekers/cli/pdf_extractor_poc.py similarity index 100% rename from cli/pdf_extractor_poc.py rename to src/skill_seekers/cli/pdf_extractor_poc.py diff --git a/cli/pdf_scraper.py b/src/skill_seekers/cli/pdf_scraper.py similarity index 100% rename from cli/pdf_scraper.py rename to src/skill_seekers/cli/pdf_scraper.py diff --git a/cli/run_tests.py b/src/skill_seekers/cli/run_tests.py similarity index 100% rename from cli/run_tests.py rename to src/skill_seekers/cli/run_tests.py diff --git a/cli/split_config.py b/src/skill_seekers/cli/split_config.py similarity index 100% rename from cli/split_config.py rename to src/skill_seekers/cli/split_config.py diff --git a/cli/test_unified_simple.py b/src/skill_seekers/cli/test_unified_simple.py similarity index 100% rename from cli/test_unified_simple.py rename to src/skill_seekers/cli/test_unified_simple.py diff --git a/cli/unified_scraper.py b/src/skill_seekers/cli/unified_scraper.py similarity index 100% rename from cli/unified_scraper.py rename to src/skill_seekers/cli/unified_scraper.py diff --git a/cli/unified_skill_builder.py b/src/skill_seekers/cli/unified_skill_builder.py similarity index 100% rename from cli/unified_skill_builder.py rename to src/skill_seekers/cli/unified_skill_builder.py diff --git a/cli/upload_skill.py b/src/skill_seekers/cli/upload_skill.py similarity index 100% rename from cli/upload_skill.py rename to src/skill_seekers/cli/upload_skill.py diff --git a/cli/utils.py b/src/skill_seekers/cli/utils.py similarity index 100% rename from cli/utils.py rename to src/skill_seekers/cli/utils.py diff --git a/skill_seeker_mcp/README.md b/src/skill_seekers/mcp/README.md similarity index 100% rename from skill_seeker_mcp/README.md rename to src/skill_seekers/mcp/README.md diff --git a/skill_seeker_mcp/__init__.py b/src/skill_seekers/mcp/__init__.py similarity index 97% rename from skill_seeker_mcp/__init__.py rename to src/skill_seekers/mcp/__init__.py index 3a9d544..4616b37 100644 --- a/skill_seeker_mcp/__init__.py +++ b/src/skill_seekers/mcp/__init__.py @@ -22,6 +22,6 @@ Usage: in ~/.config/claude-code/mcp.json """ -__version__ = "1.2.0" +__version__ = "2.0.0" __all__ = [] diff --git a/skill_seeker_mcp/requirements.txt b/src/skill_seekers/mcp/requirements.txt similarity index 100% rename from skill_seeker_mcp/requirements.txt rename to src/skill_seekers/mcp/requirements.txt diff --git a/skill_seeker_mcp/server.py b/src/skill_seekers/mcp/server.py similarity index 100% rename from skill_seeker_mcp/server.py rename to src/skill_seekers/mcp/server.py diff --git a/skill_seeker_mcp/tools/__init__.py b/src/skill_seekers/mcp/tools/__init__.py similarity index 96% rename from skill_seeker_mcp/tools/__init__.py rename to src/skill_seekers/mcp/tools/__init__.py index db462b5..388f312 100644 --- a/skill_seeker_mcp/tools/__init__.py +++ b/src/skill_seekers/mcp/tools/__init__.py @@ -14,6 +14,6 @@ Current state: This directory is a placeholder for future modularization. """ -__version__ = "1.2.0" +__version__ = "2.0.0" __all__ = [] diff --git a/tests/test_config_validation.py b/tests/test_config_validation.py index ced51d3..4bef957 100644 --- a/tests/test_config_validation.py +++ b/tests/test_config_validation.py @@ -11,7 +11,7 @@ import unittest # Add parent directory to path sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from cli.doc_scraper import validate_config +from skill_seekers.cli.doc_scraper import validate_config class TestConfigValidation(unittest.TestCase): diff --git a/tests/test_constants.py b/tests/test_constants.py index 5f9732f..0f81b74 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -8,7 +8,7 @@ from pathlib import Path # Add parent directory to path sys.path.insert(0, str(Path(__file__).parent.parent)) -from cli.constants import ( +from skill_seekers.cli.constants import ( DEFAULT_RATE_LIMIT, DEFAULT_MAX_PAGES, DEFAULT_CHECKPOINT_INTERVAL, diff --git a/tests/test_integration.py b/tests/test_integration.py index 4501eda..41086d0 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -15,7 +15,7 @@ from pathlib import Path # Add parent directory to path sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from cli.doc_scraper import DocToSkillConverter, load_config, validate_config +from skill_seekers.cli.doc_scraper import DocToSkillConverter, load_config, validate_config class TestDryRunMode(unittest.TestCase): @@ -571,7 +571,7 @@ app.use('*', cors()) mock_get.side_effect = mock_download # Run scraper - from cli.doc_scraper import DocToSkillConverter as DocumentationScraper + from skill_seekers.cli.doc_scraper import DocToSkillConverter as DocumentationScraper scraper = DocumentationScraper(config, dry_run=False) result = scraper._try_llms_txt() @@ -608,7 +608,7 @@ def test_no_content_truncation(): } # Create scraper with long content - from cli.doc_scraper import DocToSkillConverter + from skill_seekers.cli.doc_scraper import DocToSkillConverter scraper = DocToSkillConverter(config, dry_run=False) # Create page with content > 2500 chars diff --git a/tests/test_llms_txt_detector.py b/tests/test_llms_txt_detector.py index d5934d8..5d474ac 100644 --- a/tests/test_llms_txt_detector.py +++ b/tests/test_llms_txt_detector.py @@ -1,6 +1,6 @@ import pytest from unittest.mock import patch, Mock -from cli.llms_txt_detector import LlmsTxtDetector +from skill_seekers.cli.llms_txt_detector import LlmsTxtDetector def test_detect_llms_txt_variants(): """Test detection of llms.txt file variants""" diff --git a/tests/test_llms_txt_downloader.py b/tests/test_llms_txt_downloader.py index 3aaf48c..3b945fc 100644 --- a/tests/test_llms_txt_downloader.py +++ b/tests/test_llms_txt_downloader.py @@ -1,7 +1,7 @@ import pytest from unittest.mock import patch, Mock import requests -from cli.llms_txt_downloader import LlmsTxtDownloader +from skill_seekers.cli.llms_txt_downloader import LlmsTxtDownloader def test_successful_download(): """Test successful download with valid markdown content""" diff --git a/tests/test_llms_txt_parser.py b/tests/test_llms_txt_parser.py index 8e8c7fa..0a28bcb 100644 --- a/tests/test_llms_txt_parser.py +++ b/tests/test_llms_txt_parser.py @@ -1,5 +1,5 @@ import pytest -from cli.llms_txt_parser import LlmsTxtParser +from skill_seekers.cli.llms_txt_parser import LlmsTxtParser def test_parse_markdown_sections(): """Test parsing markdown into page sections""" diff --git a/tests/test_package_structure.py b/tests/test_package_structure.py index 5dffea9..0824401 100644 --- a/tests/test_package_structure.py +++ b/tests/test_package_structure.py @@ -1,7 +1,7 @@ """Test suite for Python package structure. Tests that the package structure is correct and imports work properly. -This ensures Phase 0 refactoring is successful. +This ensures modern Python packaging (src/ layout, pyproject.toml) is successful. """ import pytest @@ -10,45 +10,45 @@ from pathlib import Path class TestCliPackage: - """Test cli package structure and imports.""" + """Test skill_seekers.cli package structure and imports.""" def test_cli_package_exists(self): - """Test that cli package can be imported.""" - import cli - assert cli is not None + """Test that skill_seekers.cli package can be imported.""" + import skill_seekers.cli + assert skill_seekers.cli is not None def test_cli_has_version(self): - """Test that cli package has __version__.""" - import cli - assert hasattr(cli, '__version__') - assert cli.__version__ == '1.3.0' + """Test that skill_seekers.cli package has __version__.""" + import skill_seekers.cli + assert hasattr(skill_seekers.cli, '__version__') + assert skill_seekers.cli.__version__ == '2.0.0' def test_cli_has_all(self): - """Test that cli package has __all__ export list.""" - import cli - assert hasattr(cli, '__all__') - assert isinstance(cli.__all__, list) - assert len(cli.__all__) > 0 + """Test that skill_seekers.cli package has __all__ export list.""" + import skill_seekers.cli + assert hasattr(skill_seekers.cli, '__all__') + assert isinstance(skill_seekers.cli.__all__, list) + assert len(skill_seekers.cli.__all__) > 0 def test_llms_txt_detector_import(self): - """Test that LlmsTxtDetector can be imported from cli.""" - from cli import LlmsTxtDetector + """Test that LlmsTxtDetector can be imported from skill_seekers.cli.""" + from skill_seekers.cli import LlmsTxtDetector assert LlmsTxtDetector is not None def test_llms_txt_downloader_import(self): - """Test that LlmsTxtDownloader can be imported from cli.""" - from cli import LlmsTxtDownloader + """Test that LlmsTxtDownloader can be imported from skill_seekers.cli.""" + from skill_seekers.cli import LlmsTxtDownloader assert LlmsTxtDownloader is not None def test_llms_txt_parser_import(self): - """Test that LlmsTxtParser can be imported from cli.""" - from cli import LlmsTxtParser + """Test that LlmsTxtParser can be imported from skill_seekers.cli.""" + from skill_seekers.cli import LlmsTxtParser assert LlmsTxtParser is not None def test_open_folder_import(self): - """Test that open_folder can be imported from cli (if utils exists).""" + """Test that open_folder can be imported from skill_seekers.cli (if utils exists).""" try: - from cli import open_folder + from skill_seekers.cli import open_folder # If import succeeds, function should not be None assert open_folder is not None except ImportError: @@ -57,7 +57,7 @@ class TestCliPackage: def test_cli_exports_match_all(self): """Test that exported items in __all__ can actually be imported.""" - import cli + import skill_seekers.cli as cli for item_name in cli.__all__: if item_name == 'open_folder' and cli.open_folder is None: # open_folder might be None if utils doesn't exist @@ -66,66 +66,66 @@ class TestCliPackage: class TestMcpPackage: - """Test skill_seeker_mcp package structure and imports.""" + """Test skill_seekers.mcp package structure and imports.""" def test_mcp_package_exists(self): - """Test that skill_seeker_mcp package can be imported.""" - import skill_seeker_mcp - assert skill_seeker_mcp is not None + """Test that skill_seekers.mcp package can be imported.""" + import skill_seekers.mcp + assert skill_seekers.mcp is not None def test_mcp_has_version(self): - """Test that skill_seeker_mcp package has __version__.""" - import skill_seeker_mcp - assert hasattr(skill_seeker_mcp, '__version__') - assert skill_seeker_mcp.__version__ == '1.2.0' + """Test that skill_seekers.mcp package has __version__.""" + import skill_seekers.mcp + assert hasattr(skill_seekers.mcp, '__version__') + assert skill_seekers.mcp.__version__ == '2.0.0' def test_mcp_has_all(self): - """Test that skill_seeker_mcp package has __all__ export list.""" - import skill_seeker_mcp - assert hasattr(skill_seeker_mcp, '__all__') - assert isinstance(skill_seeker_mcp.__all__, list) + """Test that skill_seekers.mcp package has __all__ export list.""" + import skill_seekers.mcp + assert hasattr(skill_seekers.mcp, '__all__') + assert isinstance(skill_seekers.mcp.__all__, list) def test_mcp_tools_package_exists(self): - """Test that skill_seeker_mcp.tools subpackage can be imported.""" - import skill_seeker_mcp.tools - assert skill_seeker_mcp.tools is not None + """Test that skill_seekers.mcp.tools subpackage can be imported.""" + import skill_seekers.mcp.tools + assert skill_seekers.mcp.tools is not None def test_mcp_tools_has_version(self): - """Test that skill_seeker_mcp.tools has __version__.""" - import skill_seeker_mcp.tools - assert hasattr(skill_seeker_mcp.tools, '__version__') - assert skill_seeker_mcp.tools.__version__ == '1.2.0' + """Test that skill_seekers.mcp.tools has __version__.""" + import skill_seekers.mcp.tools + assert hasattr(skill_seekers.mcp.tools, '__version__') + assert skill_seekers.mcp.tools.__version__ == '2.0.0' class TestPackageStructure: - """Test overall package structure integrity.""" + """Test overall package structure integrity (src/ layout).""" def test_cli_init_file_exists(self): - """Test that cli/__init__.py exists.""" - init_file = Path(__file__).parent.parent / 'cli' / '__init__.py' - assert init_file.exists(), "cli/__init__.py not found" + """Test that src/skill_seekers/cli/__init__.py exists.""" + init_file = Path(__file__).parent.parent / 'src' / 'skill_seekers' / 'cli' / '__init__.py' + assert init_file.exists(), "src/skill_seekers/cli/__init__.py not found" def test_mcp_init_file_exists(self): - """Test that skill_seeker_mcp/__init__.py exists.""" - init_file = Path(__file__).parent.parent / 'skill_seeker_mcp' / '__init__.py' - assert init_file.exists(), "skill_seeker_mcp/__init__.py not found" + """Test that src/skill_seekers/mcp/__init__.py exists.""" + init_file = Path(__file__).parent.parent / 'src' / 'skill_seekers' / 'mcp' / '__init__.py' + assert init_file.exists(), "src/skill_seekers/mcp/__init__.py not found" def test_mcp_tools_init_file_exists(self): - """Test that skill_seeker_mcp/tools/__init__.py exists.""" - init_file = Path(__file__).parent.parent / 'skill_seeker_mcp' / 'tools' / '__init__.py' - assert init_file.exists(), "skill_seeker_mcp/tools/__init__.py not found" + """Test that src/skill_seekers/mcp/tools/__init__.py exists.""" + init_file = Path(__file__).parent.parent / 'src' / 'skill_seekers' / 'mcp' / 'tools' / '__init__.py' + assert init_file.exists(), "src/skill_seekers/mcp/tools/__init__.py not found" def test_cli_init_has_docstring(self): - """Test that cli/__init__.py has a module docstring.""" - import cli - assert cli.__doc__ is not None - assert len(cli.__doc__) > 50 # Should have substantial documentation + """Test that skill_seekers.cli/__init__.py has a module docstring.""" + import skill_seekers.cli + assert skill_seekers.cli.__doc__ is not None + assert len(skill_seekers.cli.__doc__) > 50 # Should have substantial documentation def test_mcp_init_has_docstring(self): - """Test that skill_seeker_mcp/__init__.py has a module docstring.""" - import skill_seeker_mcp - assert skill_seeker_mcp.__doc__ is not None - assert len(skill_seeker_mcp.__doc__) > 50 # Should have substantial documentation + """Test that skill_seekers.mcp/__init__.py has a module docstring.""" + import skill_seekers.mcp + assert skill_seekers.mcp.__doc__ is not None + assert len(skill_seekers.mcp.__doc__) > 50 # Should have substantial documentation class TestImportPatterns: @@ -133,28 +133,30 @@ class TestImportPatterns: def test_direct_module_import(self): """Test importing modules directly.""" - from cli import llms_txt_detector - from cli import llms_txt_downloader - from cli import llms_txt_parser + from skill_seekers.cli import llms_txt_detector + from skill_seekers.cli import llms_txt_downloader + from skill_seekers.cli import llms_txt_parser assert llms_txt_detector is not None assert llms_txt_downloader is not None assert llms_txt_parser is not None def test_class_import_from_package(self): """Test importing classes from package.""" - from cli import LlmsTxtDetector, LlmsTxtDownloader, LlmsTxtParser + from skill_seekers.cli import LlmsTxtDetector, LlmsTxtDownloader, LlmsTxtParser assert LlmsTxtDetector.__name__ == 'LlmsTxtDetector' assert LlmsTxtDownloader.__name__ == 'LlmsTxtDownloader' assert LlmsTxtParser.__name__ == 'LlmsTxtParser' def test_package_level_import(self): """Test importing entire packages.""" - import cli - import skill_seeker_mcp - import skill_seeker_mcp.tools - assert 'cli' in sys.modules - assert 'skill_seeker_mcp' in sys.modules - assert 'skill_seeker_mcp.tools' in sys.modules + import skill_seekers + import skill_seekers.cli + import skill_seekers.mcp + import skill_seekers.mcp.tools + assert 'skill_seekers' in sys.modules + assert 'skill_seekers.cli' in sys.modules + assert 'skill_seekers.mcp' in sys.modules + assert 'skill_seekers.mcp.tools' in sys.modules class TestBackwardsCompatibility: @@ -163,22 +165,59 @@ class TestBackwardsCompatibility: def test_direct_file_import_still_works(self): """Test that direct file imports still work (backwards compatible).""" # This ensures we didn't break existing code - from cli.llms_txt_detector import LlmsTxtDetector - from cli.llms_txt_downloader import LlmsTxtDownloader - from cli.llms_txt_parser import LlmsTxtParser + from skill_seekers.cli.llms_txt_detector import LlmsTxtDetector + from skill_seekers.cli.llms_txt_downloader import LlmsTxtDownloader + from skill_seekers.cli.llms_txt_parser import LlmsTxtParser assert LlmsTxtDetector is not None assert LlmsTxtDownloader is not None assert LlmsTxtParser is not None def test_module_path_import_still_works(self): - """Test that module-level imports still work.""" - import cli.llms_txt_detector as detector - import cli.llms_txt_downloader as downloader - import cli.llms_txt_parser as parser - assert detector is not None - assert downloader is not None + """Test that full module path imports still work.""" + import skill_seekers.cli.llms_txt_detector + import skill_seekers.cli.llms_txt_downloader + import skill_seekers.cli.llms_txt_parser + assert skill_seekers.cli.llms_txt_detector is not None + assert skill_seekers.cli.llms_txt_downloader is not None + assert skill_seekers.cli.llms_txt_parser is not None + + +class TestRootPackage: + """Test root skill_seekers package.""" + + def test_root_package_exists(self): + """Test that skill_seekers root package can be imported.""" + import skill_seekers + assert skill_seekers is not None + + def test_root_has_version(self): + """Test that skill_seekers root package has __version__.""" + import skill_seekers + assert hasattr(skill_seekers, '__version__') + assert skill_seekers.__version__ == '2.0.0' + + def test_root_has_metadata(self): + """Test that skill_seekers root package has metadata.""" + import skill_seekers + assert hasattr(skill_seekers, '__author__') + assert hasattr(skill_seekers, '__license__') + assert skill_seekers.__license__ == 'MIT' + + +class TestCLIEntryPoints: + """Test that CLI entry points are properly configured.""" + + def test_main_cli_module_exists(self): + """Test that main.py module exists and can be imported.""" + from skill_seekers.cli import main + assert main is not None + assert hasattr(main, 'main') + assert callable(main.main) + + def test_main_cli_has_parser(self): + """Test that main.py has parser creation function.""" + from skill_seekers.cli.main import create_parser + parser = create_parser() assert parser is not None - - -if __name__ == '__main__': - pytest.main([__file__, '-v']) + # Test that main subcommands are configured + assert parser.prog == 'skill-seekers' diff --git a/tests/test_scraper_features.py b/tests/test_scraper_features.py index 2ca0bd9..a4b789e 100644 --- a/tests/test_scraper_features.py +++ b/tests/test_scraper_features.py @@ -13,7 +13,7 @@ from bs4 import BeautifulSoup # Add parent directory to path sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from cli.doc_scraper import DocToSkillConverter +from skill_seekers.cli.doc_scraper import DocToSkillConverter class TestURLValidation(unittest.TestCase): diff --git a/tests/test_terminal_detection.py b/tests/test_terminal_detection.py index a59545a..e07787e 100644 --- a/tests/test_terminal_detection.py +++ b/tests/test_terminal_detection.py @@ -14,7 +14,7 @@ from pathlib import Path # Add parent directory to path for imports sys.path.insert(0, str(Path(__file__).parent.parent)) -from cli.enhance_skill_local import detect_terminal_app, LocalSkillEnhancer +from skill_seekers.cli.enhance_skill_local import detect_terminal_app, LocalSkillEnhancer class TestDetectTerminalApp(unittest.TestCase): @@ -298,7 +298,7 @@ class TestTerminalMapCompleteness(unittest.TestCase): def test_terminal_map_has_all_documented_terminals(self): """Verify TERMINAL_MAP contains all terminals mentioned in documentation.""" - from cli.enhance_skill_local import detect_terminal_app + from skill_seekers.cli.enhance_skill_local import detect_terminal_app # Get the TERMINAL_MAP from the function's scope # We need to test this indirectly by checking each known terminal