skill-seekers 2.7.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- skill_seekers/__init__.py +22 -0
- skill_seekers/cli/__init__.py +39 -0
- skill_seekers/cli/adaptors/__init__.py +120 -0
- skill_seekers/cli/adaptors/base.py +221 -0
- skill_seekers/cli/adaptors/claude.py +485 -0
- skill_seekers/cli/adaptors/gemini.py +453 -0
- skill_seekers/cli/adaptors/markdown.py +269 -0
- skill_seekers/cli/adaptors/openai.py +503 -0
- skill_seekers/cli/ai_enhancer.py +310 -0
- skill_seekers/cli/api_reference_builder.py +373 -0
- skill_seekers/cli/architectural_pattern_detector.py +525 -0
- skill_seekers/cli/code_analyzer.py +1462 -0
- skill_seekers/cli/codebase_scraper.py +1225 -0
- skill_seekers/cli/config_command.py +563 -0
- skill_seekers/cli/config_enhancer.py +431 -0
- skill_seekers/cli/config_extractor.py +871 -0
- skill_seekers/cli/config_manager.py +452 -0
- skill_seekers/cli/config_validator.py +394 -0
- skill_seekers/cli/conflict_detector.py +528 -0
- skill_seekers/cli/constants.py +72 -0
- skill_seekers/cli/dependency_analyzer.py +757 -0
- skill_seekers/cli/doc_scraper.py +2332 -0
- skill_seekers/cli/enhance_skill.py +488 -0
- skill_seekers/cli/enhance_skill_local.py +1096 -0
- skill_seekers/cli/enhance_status.py +194 -0
- skill_seekers/cli/estimate_pages.py +433 -0
- skill_seekers/cli/generate_router.py +1209 -0
- skill_seekers/cli/github_fetcher.py +534 -0
- skill_seekers/cli/github_scraper.py +1466 -0
- skill_seekers/cli/guide_enhancer.py +723 -0
- skill_seekers/cli/how_to_guide_builder.py +1267 -0
- skill_seekers/cli/install_agent.py +461 -0
- skill_seekers/cli/install_skill.py +178 -0
- skill_seekers/cli/language_detector.py +614 -0
- skill_seekers/cli/llms_txt_detector.py +60 -0
- skill_seekers/cli/llms_txt_downloader.py +104 -0
- skill_seekers/cli/llms_txt_parser.py +150 -0
- skill_seekers/cli/main.py +558 -0
- skill_seekers/cli/markdown_cleaner.py +132 -0
- skill_seekers/cli/merge_sources.py +806 -0
- skill_seekers/cli/package_multi.py +77 -0
- skill_seekers/cli/package_skill.py +241 -0
- skill_seekers/cli/pattern_recognizer.py +1825 -0
- skill_seekers/cli/pdf_extractor_poc.py +1166 -0
- skill_seekers/cli/pdf_scraper.py +617 -0
- skill_seekers/cli/quality_checker.py +519 -0
- skill_seekers/cli/rate_limit_handler.py +438 -0
- skill_seekers/cli/resume_command.py +160 -0
- skill_seekers/cli/run_tests.py +230 -0
- skill_seekers/cli/setup_wizard.py +93 -0
- skill_seekers/cli/split_config.py +390 -0
- skill_seekers/cli/swift_patterns.py +560 -0
- skill_seekers/cli/test_example_extractor.py +1081 -0
- skill_seekers/cli/test_unified_simple.py +179 -0
- skill_seekers/cli/unified_codebase_analyzer.py +572 -0
- skill_seekers/cli/unified_scraper.py +932 -0
- skill_seekers/cli/unified_skill_builder.py +1605 -0
- skill_seekers/cli/upload_skill.py +162 -0
- skill_seekers/cli/utils.py +432 -0
- skill_seekers/mcp/__init__.py +33 -0
- skill_seekers/mcp/agent_detector.py +316 -0
- skill_seekers/mcp/git_repo.py +273 -0
- skill_seekers/mcp/server.py +231 -0
- skill_seekers/mcp/server_fastmcp.py +1249 -0
- skill_seekers/mcp/server_legacy.py +2302 -0
- skill_seekers/mcp/source_manager.py +285 -0
- skill_seekers/mcp/tools/__init__.py +115 -0
- skill_seekers/mcp/tools/config_tools.py +251 -0
- skill_seekers/mcp/tools/packaging_tools.py +826 -0
- skill_seekers/mcp/tools/scraping_tools.py +842 -0
- skill_seekers/mcp/tools/source_tools.py +828 -0
- skill_seekers/mcp/tools/splitting_tools.py +212 -0
- skill_seekers/py.typed +0 -0
- skill_seekers-2.7.3.dist-info/METADATA +2027 -0
- skill_seekers-2.7.3.dist-info/RECORD +79 -0
- skill_seekers-2.7.3.dist-info/WHEEL +5 -0
- skill_seekers-2.7.3.dist-info/entry_points.txt +19 -0
- skill_seekers-2.7.3.dist-info/licenses/LICENSE +21 -0
- skill_seekers-2.7.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Automatic Skill Uploader
|
|
4
|
+
Uploads a skill package to LLM platforms (Claude, Gemini, OpenAI, etc.)
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
# Claude (default)
|
|
8
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
9
|
+
skill-seekers upload output/react.zip
|
|
10
|
+
|
|
11
|
+
# Gemini
|
|
12
|
+
export GOOGLE_API_KEY=AIzaSy...
|
|
13
|
+
skill-seekers upload output/react-gemini.tar.gz --target gemini
|
|
14
|
+
|
|
15
|
+
# OpenAI
|
|
16
|
+
export OPENAI_API_KEY=sk-proj-...
|
|
17
|
+
skill-seekers upload output/react-openai.zip --target openai
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import os
|
|
22
|
+
import sys
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
# Import utilities
|
|
26
|
+
try:
|
|
27
|
+
from utils import print_upload_instructions
|
|
28
|
+
except ImportError:
|
|
29
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
30
|
+
from utils import print_upload_instructions
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def upload_skill_api(package_path, target="claude", api_key=None):
|
|
34
|
+
"""
|
|
35
|
+
Upload skill package to LLM platform
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
package_path: Path to skill package file
|
|
39
|
+
target: Target platform ('claude', 'gemini', 'openai')
|
|
40
|
+
api_key: Optional API key (otherwise read from environment)
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
tuple: (success, message)
|
|
44
|
+
"""
|
|
45
|
+
try:
|
|
46
|
+
from skill_seekers.cli.adaptors import get_adaptor
|
|
47
|
+
except ImportError:
|
|
48
|
+
return False, "Adaptor system not available. Reinstall skill-seekers."
|
|
49
|
+
|
|
50
|
+
# Get platform-specific adaptor
|
|
51
|
+
try:
|
|
52
|
+
adaptor = get_adaptor(target)
|
|
53
|
+
except ValueError as e:
|
|
54
|
+
return False, str(e)
|
|
55
|
+
|
|
56
|
+
# Get API key
|
|
57
|
+
if not api_key:
|
|
58
|
+
api_key = os.environ.get(adaptor.get_env_var_name(), "").strip()
|
|
59
|
+
|
|
60
|
+
if not api_key:
|
|
61
|
+
return False, f"{adaptor.get_env_var_name()} not set. Export your API key first."
|
|
62
|
+
|
|
63
|
+
# Validate API key format
|
|
64
|
+
if not adaptor.validate_api_key(api_key):
|
|
65
|
+
return False, f"Invalid API key format for {adaptor.PLATFORM_NAME}"
|
|
66
|
+
|
|
67
|
+
package_path = Path(package_path)
|
|
68
|
+
|
|
69
|
+
# Basic file validation
|
|
70
|
+
if not package_path.exists():
|
|
71
|
+
return False, f"File not found: {package_path}"
|
|
72
|
+
|
|
73
|
+
skill_name = package_path.stem
|
|
74
|
+
|
|
75
|
+
print(f"📤 Uploading skill: {skill_name}")
|
|
76
|
+
print(f" Target: {adaptor.PLATFORM_NAME}")
|
|
77
|
+
print(f" Source: {package_path}")
|
|
78
|
+
print(f" Size: {package_path.stat().st_size:,} bytes")
|
|
79
|
+
print()
|
|
80
|
+
|
|
81
|
+
# Upload using adaptor
|
|
82
|
+
print(f"⏳ Uploading to {adaptor.PLATFORM_NAME}...")
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
result = adaptor.upload(package_path, api_key)
|
|
86
|
+
|
|
87
|
+
if result["success"]:
|
|
88
|
+
print()
|
|
89
|
+
print(f"✅ {result['message']}")
|
|
90
|
+
print()
|
|
91
|
+
if result["url"]:
|
|
92
|
+
print("Your skill is now available at:")
|
|
93
|
+
print(f" {result['url']}")
|
|
94
|
+
if result["skill_id"]:
|
|
95
|
+
print(f" Skill ID: {result['skill_id']}")
|
|
96
|
+
print()
|
|
97
|
+
return True, "Upload successful"
|
|
98
|
+
else:
|
|
99
|
+
return False, result["message"]
|
|
100
|
+
|
|
101
|
+
except Exception as e:
|
|
102
|
+
return False, f"Unexpected error: {str(e)}"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def main():
|
|
106
|
+
parser = argparse.ArgumentParser(
|
|
107
|
+
description="Upload a skill package to LLM platforms",
|
|
108
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
109
|
+
epilog="""
|
|
110
|
+
Setup:
|
|
111
|
+
Claude:
|
|
112
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
113
|
+
|
|
114
|
+
Gemini:
|
|
115
|
+
export GOOGLE_API_KEY=AIzaSy...
|
|
116
|
+
|
|
117
|
+
OpenAI:
|
|
118
|
+
export OPENAI_API_KEY=sk-proj-...
|
|
119
|
+
|
|
120
|
+
Examples:
|
|
121
|
+
# Upload to Claude (default)
|
|
122
|
+
skill-seekers upload output/react.zip
|
|
123
|
+
|
|
124
|
+
# Upload to Gemini
|
|
125
|
+
skill-seekers upload output/react-gemini.tar.gz --target gemini
|
|
126
|
+
|
|
127
|
+
# Upload to OpenAI
|
|
128
|
+
skill-seekers upload output/react-openai.zip --target openai
|
|
129
|
+
|
|
130
|
+
# Upload with explicit API key
|
|
131
|
+
skill-seekers upload output/react.zip --api-key sk-ant-...
|
|
132
|
+
""",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
parser.add_argument("package_file", help="Path to skill package file (e.g., output/react.zip)")
|
|
136
|
+
|
|
137
|
+
parser.add_argument(
|
|
138
|
+
"--target",
|
|
139
|
+
choices=["claude", "gemini", "openai"],
|
|
140
|
+
default="claude",
|
|
141
|
+
help="Target LLM platform (default: claude)",
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
parser.add_argument("--api-key", help="Platform API key (or set environment variable)")
|
|
145
|
+
|
|
146
|
+
args = parser.parse_args()
|
|
147
|
+
|
|
148
|
+
# Upload skill
|
|
149
|
+
success, message = upload_skill_api(args.package_file, args.target, args.api_key)
|
|
150
|
+
|
|
151
|
+
if success:
|
|
152
|
+
sys.exit(0)
|
|
153
|
+
else:
|
|
154
|
+
print(f"\n❌ Upload failed: {message}")
|
|
155
|
+
print()
|
|
156
|
+
print("📝 Manual upload instructions:")
|
|
157
|
+
print_upload_instructions(args.package_file)
|
|
158
|
+
sys.exit(1)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
if __name__ == "__main__":
|
|
162
|
+
main()
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Utility functions for Skill Seeker CLI tools
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import platform
|
|
9
|
+
import subprocess
|
|
10
|
+
import time
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import TypeVar
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
T = TypeVar("T")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def open_folder(folder_path: str | Path) -> bool:
|
|
21
|
+
"""
|
|
22
|
+
Open a folder in the system file browser
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
folder_path: Path to folder to open
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
bool: True if successful, False otherwise
|
|
29
|
+
"""
|
|
30
|
+
folder_path = Path(folder_path).resolve()
|
|
31
|
+
|
|
32
|
+
if not folder_path.exists():
|
|
33
|
+
print(f"⚠️ Folder not found: {folder_path}")
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
system = platform.system()
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
if system == "Linux":
|
|
40
|
+
# Try xdg-open first (standard)
|
|
41
|
+
subprocess.run(["xdg-open", str(folder_path)], check=True)
|
|
42
|
+
elif system == "Darwin": # macOS
|
|
43
|
+
subprocess.run(["open", str(folder_path)], check=True)
|
|
44
|
+
elif system == "Windows":
|
|
45
|
+
subprocess.run(["explorer", str(folder_path)], check=True)
|
|
46
|
+
else:
|
|
47
|
+
print(f"⚠️ Unknown operating system: {system}")
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
return True
|
|
51
|
+
|
|
52
|
+
except subprocess.CalledProcessError:
|
|
53
|
+
print("⚠️ Could not open folder automatically")
|
|
54
|
+
return False
|
|
55
|
+
except FileNotFoundError:
|
|
56
|
+
print("⚠️ File browser not found on system")
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def has_api_key() -> bool:
|
|
61
|
+
"""
|
|
62
|
+
Check if ANTHROPIC_API_KEY is set in environment
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
bool: True if API key is set, False otherwise
|
|
66
|
+
"""
|
|
67
|
+
api_key = os.environ.get("ANTHROPIC_API_KEY", "").strip()
|
|
68
|
+
return len(api_key) > 0
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_api_key() -> str | None:
|
|
72
|
+
"""
|
|
73
|
+
Get ANTHROPIC_API_KEY from environment
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
str: API key or None if not set
|
|
77
|
+
"""
|
|
78
|
+
api_key = os.environ.get("ANTHROPIC_API_KEY", "").strip()
|
|
79
|
+
return api_key if api_key else None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def get_upload_url() -> str:
|
|
83
|
+
"""
|
|
84
|
+
Get the Claude skills upload URL
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
str: Claude skills upload URL
|
|
88
|
+
"""
|
|
89
|
+
return "https://claude.ai/skills"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def print_upload_instructions(zip_path: str | Path) -> None:
|
|
93
|
+
"""
|
|
94
|
+
Print clear upload instructions for manual upload
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
zip_path: Path to the .zip file to upload
|
|
98
|
+
"""
|
|
99
|
+
zip_path = Path(zip_path)
|
|
100
|
+
|
|
101
|
+
print()
|
|
102
|
+
print("╔══════════════════════════════════════════════════════════╗")
|
|
103
|
+
print("║ NEXT STEP ║")
|
|
104
|
+
print("╚══════════════════════════════════════════════════════════╝")
|
|
105
|
+
print()
|
|
106
|
+
print(f"📤 Upload to Claude: {get_upload_url()}")
|
|
107
|
+
print()
|
|
108
|
+
print(f"1. Go to {get_upload_url()}")
|
|
109
|
+
print('2. Click "Upload Skill"')
|
|
110
|
+
print(f"3. Select: {zip_path}")
|
|
111
|
+
print("4. Done! ✅")
|
|
112
|
+
print()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def format_file_size(size_bytes: int) -> str:
|
|
116
|
+
"""
|
|
117
|
+
Format file size in human-readable format
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
size_bytes: Size in bytes
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
str: Formatted size (e.g., "45.3 KB")
|
|
124
|
+
"""
|
|
125
|
+
if size_bytes < 1024:
|
|
126
|
+
return f"{size_bytes} bytes"
|
|
127
|
+
elif size_bytes < 1024 * 1024:
|
|
128
|
+
return f"{size_bytes / 1024:.1f} KB"
|
|
129
|
+
else:
|
|
130
|
+
return f"{size_bytes / (1024 * 1024):.1f} MB"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def validate_skill_directory(skill_dir: str | Path) -> tuple[bool, str | None]:
|
|
134
|
+
"""
|
|
135
|
+
Validate that a directory is a valid skill directory
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
skill_dir: Path to skill directory
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
tuple: (is_valid, error_message)
|
|
142
|
+
"""
|
|
143
|
+
skill_path = Path(skill_dir)
|
|
144
|
+
|
|
145
|
+
if not skill_path.exists():
|
|
146
|
+
return False, f"Directory not found: {skill_dir}"
|
|
147
|
+
|
|
148
|
+
if not skill_path.is_dir():
|
|
149
|
+
return False, f"Not a directory: {skill_dir}"
|
|
150
|
+
|
|
151
|
+
skill_md = skill_path / "SKILL.md"
|
|
152
|
+
if not skill_md.exists():
|
|
153
|
+
return False, f"SKILL.md not found in {skill_dir}"
|
|
154
|
+
|
|
155
|
+
return True, None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def validate_zip_file(zip_path: str | Path) -> tuple[bool, str | None]:
|
|
159
|
+
"""
|
|
160
|
+
Validate that a file is a valid skill .zip file
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
zip_path: Path to .zip file
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
tuple: (is_valid, error_message)
|
|
167
|
+
"""
|
|
168
|
+
zip_path = Path(zip_path)
|
|
169
|
+
|
|
170
|
+
if not zip_path.exists():
|
|
171
|
+
return False, f"File not found: {zip_path}"
|
|
172
|
+
|
|
173
|
+
if not zip_path.is_file():
|
|
174
|
+
return False, f"Not a file: {zip_path}"
|
|
175
|
+
|
|
176
|
+
if zip_path.suffix != ".zip":
|
|
177
|
+
return False, f"Not a .zip file: {zip_path}"
|
|
178
|
+
|
|
179
|
+
return True, None
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def read_reference_files(
|
|
183
|
+
skill_dir: str | Path, max_chars: int = 100000, preview_limit: int = 40000
|
|
184
|
+
) -> dict[str, dict]:
|
|
185
|
+
"""Read reference files from a skill directory with enriched metadata.
|
|
186
|
+
|
|
187
|
+
This function reads markdown files from the references/ subdirectory
|
|
188
|
+
of a skill, applying both per-file and total content limits.
|
|
189
|
+
Returns enriched metadata including source type, confidence, and path.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
skill_dir (str or Path): Path to skill directory
|
|
193
|
+
max_chars (int): Maximum total characters to read (default: 100000)
|
|
194
|
+
preview_limit (int): Maximum characters per file (default: 40000)
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
dict: Dictionary mapping filename to metadata dict with keys:
|
|
198
|
+
- 'content': File content
|
|
199
|
+
- 'source': Source type (documentation/github/pdf/api/codebase_analysis)
|
|
200
|
+
- 'confidence': Confidence level (high/medium/low)
|
|
201
|
+
- 'path': Relative path from references directory
|
|
202
|
+
- 'repo_id': Repository identifier for multi-source (e.g., 'encode_httpx'), None for single-source
|
|
203
|
+
|
|
204
|
+
Example:
|
|
205
|
+
>>> refs = read_reference_files('output/react/', max_chars=50000)
|
|
206
|
+
>>> refs['documentation/api.md']['source']
|
|
207
|
+
'documentation'
|
|
208
|
+
>>> refs['documentation/api.md']['confidence']
|
|
209
|
+
'high'
|
|
210
|
+
"""
|
|
211
|
+
from pathlib import Path
|
|
212
|
+
|
|
213
|
+
skill_path = Path(skill_dir)
|
|
214
|
+
references_dir = skill_path / "references"
|
|
215
|
+
references: dict[str, dict] = {}
|
|
216
|
+
|
|
217
|
+
if not references_dir.exists():
|
|
218
|
+
print(f"⚠ No references directory found at {references_dir}")
|
|
219
|
+
return references
|
|
220
|
+
|
|
221
|
+
def _determine_source_metadata(relative_path: Path) -> tuple[str, str, str | None]:
|
|
222
|
+
"""Determine source type, confidence level, and repo_id from path.
|
|
223
|
+
|
|
224
|
+
For multi-source support, extracts repo_id from paths like:
|
|
225
|
+
- codebase_analysis/encode_httpx/ARCHITECTURE.md -> repo_id='encode_httpx'
|
|
226
|
+
- github/README.md -> repo_id=None (single source)
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
tuple: (source_type, confidence_level, repo_id)
|
|
230
|
+
"""
|
|
231
|
+
path_str = str(relative_path)
|
|
232
|
+
repo_id = None # Default: no repo identity
|
|
233
|
+
|
|
234
|
+
# Documentation sources (official docs)
|
|
235
|
+
if path_str.startswith("documentation/"):
|
|
236
|
+
return "documentation", "high", None
|
|
237
|
+
|
|
238
|
+
# GitHub sources
|
|
239
|
+
elif path_str.startswith("github/"):
|
|
240
|
+
# README and releases are medium confidence
|
|
241
|
+
if "README" in path_str or "releases" in path_str:
|
|
242
|
+
return "github", "medium", None
|
|
243
|
+
# Issues are low confidence (user reports)
|
|
244
|
+
elif "issues" in path_str:
|
|
245
|
+
return "github", "low", None
|
|
246
|
+
else:
|
|
247
|
+
return "github", "medium", None
|
|
248
|
+
|
|
249
|
+
# PDF sources (books, manuals)
|
|
250
|
+
elif path_str.startswith("pdf/"):
|
|
251
|
+
return "pdf", "high", None
|
|
252
|
+
|
|
253
|
+
# Merged API (synthesized from multiple sources)
|
|
254
|
+
elif path_str.startswith("api/"):
|
|
255
|
+
return "api", "high", None
|
|
256
|
+
|
|
257
|
+
# Codebase analysis (C3.x automated analysis)
|
|
258
|
+
elif path_str.startswith("codebase_analysis/"):
|
|
259
|
+
# Extract repo_id from path: codebase_analysis/{repo_id}/...
|
|
260
|
+
parts = Path(path_str).parts
|
|
261
|
+
if len(parts) >= 2:
|
|
262
|
+
repo_id = parts[1] # e.g., 'encode_httpx', 'encode_httpcore'
|
|
263
|
+
|
|
264
|
+
# ARCHITECTURE.md is high confidence (comprehensive)
|
|
265
|
+
if "ARCHITECTURE" in path_str:
|
|
266
|
+
return "codebase_analysis", "high", repo_id
|
|
267
|
+
# Patterns and examples are medium (heuristic-based)
|
|
268
|
+
elif "patterns" in path_str or "examples" in path_str:
|
|
269
|
+
return "codebase_analysis", "medium", repo_id
|
|
270
|
+
# Configuration is high (direct extraction)
|
|
271
|
+
elif "configuration" in path_str:
|
|
272
|
+
return "codebase_analysis", "high", repo_id
|
|
273
|
+
else:
|
|
274
|
+
return "codebase_analysis", "medium", repo_id
|
|
275
|
+
|
|
276
|
+
# Conflicts report (discrepancy detection)
|
|
277
|
+
elif "conflicts" in path_str:
|
|
278
|
+
return "conflicts", "medium", None
|
|
279
|
+
|
|
280
|
+
# Fallback
|
|
281
|
+
else:
|
|
282
|
+
return "unknown", "medium", None
|
|
283
|
+
|
|
284
|
+
total_chars = 0
|
|
285
|
+
# Search recursively for all .md files (including subdirectories like github/README.md)
|
|
286
|
+
for ref_file in sorted(references_dir.rglob("*.md")):
|
|
287
|
+
# Note: We now include index.md files as they contain important content
|
|
288
|
+
# (patterns, examples, configuration analysis)
|
|
289
|
+
|
|
290
|
+
content = ref_file.read_text(encoding="utf-8")
|
|
291
|
+
|
|
292
|
+
# Limit size per file
|
|
293
|
+
truncated = False
|
|
294
|
+
if len(content) > preview_limit:
|
|
295
|
+
content = content[:preview_limit] + "\n\n[Content truncated...]"
|
|
296
|
+
truncated = True
|
|
297
|
+
|
|
298
|
+
# Use relative path from references_dir as key for nested files
|
|
299
|
+
relative_path = ref_file.relative_to(references_dir)
|
|
300
|
+
source_type, confidence, repo_id = _determine_source_metadata(relative_path)
|
|
301
|
+
|
|
302
|
+
# Build enriched metadata (with repo_id for multi-source support)
|
|
303
|
+
references[str(relative_path)] = {
|
|
304
|
+
"content": content,
|
|
305
|
+
"source": source_type,
|
|
306
|
+
"confidence": confidence,
|
|
307
|
+
"path": str(relative_path),
|
|
308
|
+
"truncated": truncated,
|
|
309
|
+
"size": len(content),
|
|
310
|
+
"repo_id": repo_id, # None for single-source, repo identifier for multi-source
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
total_chars += len(content)
|
|
314
|
+
|
|
315
|
+
# Stop if we've read enough
|
|
316
|
+
if total_chars > max_chars:
|
|
317
|
+
print(f" ℹ Limiting input to {max_chars:,} characters")
|
|
318
|
+
break
|
|
319
|
+
|
|
320
|
+
return references
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def retry_with_backoff(
|
|
324
|
+
operation: Callable[[], T],
|
|
325
|
+
max_attempts: int = 3,
|
|
326
|
+
base_delay: float = 1.0,
|
|
327
|
+
operation_name: str = "operation",
|
|
328
|
+
) -> T:
|
|
329
|
+
"""Retry an operation with exponential backoff.
|
|
330
|
+
|
|
331
|
+
Useful for network operations that may fail due to transient errors.
|
|
332
|
+
Waits progressively longer between retries (exponential backoff).
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
operation: Function to retry (takes no arguments, returns result)
|
|
336
|
+
max_attempts: Maximum number of attempts (default: 3)
|
|
337
|
+
base_delay: Base delay in seconds, doubles each retry (default: 1.0)
|
|
338
|
+
operation_name: Name for logging purposes (default: "operation")
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
Result of successful operation
|
|
342
|
+
|
|
343
|
+
Raises:
|
|
344
|
+
Exception: Last exception if all retries fail
|
|
345
|
+
|
|
346
|
+
Example:
|
|
347
|
+
>>> def fetch_page():
|
|
348
|
+
... response = requests.get(url, timeout=30)
|
|
349
|
+
... response.raise_for_status()
|
|
350
|
+
... return response.text
|
|
351
|
+
>>> content = retry_with_backoff(fetch_page, max_attempts=3, operation_name=f"fetch {url}")
|
|
352
|
+
"""
|
|
353
|
+
last_exception: Exception | None = None
|
|
354
|
+
|
|
355
|
+
for attempt in range(1, max_attempts + 1):
|
|
356
|
+
try:
|
|
357
|
+
return operation()
|
|
358
|
+
except Exception as e:
|
|
359
|
+
last_exception = e
|
|
360
|
+
if attempt < max_attempts:
|
|
361
|
+
delay = base_delay * (2 ** (attempt - 1))
|
|
362
|
+
logger.warning(
|
|
363
|
+
"%s failed (attempt %d/%d), retrying in %.1fs: %s",
|
|
364
|
+
operation_name,
|
|
365
|
+
attempt,
|
|
366
|
+
max_attempts,
|
|
367
|
+
delay,
|
|
368
|
+
e,
|
|
369
|
+
)
|
|
370
|
+
time.sleep(delay)
|
|
371
|
+
else:
|
|
372
|
+
logger.error("%s failed after %d attempts: %s", operation_name, max_attempts, e)
|
|
373
|
+
|
|
374
|
+
# This should always have a value, but mypy doesn't know that
|
|
375
|
+
if last_exception is not None:
|
|
376
|
+
raise last_exception
|
|
377
|
+
raise RuntimeError(f"{operation_name} failed with no exception captured")
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
async def retry_with_backoff_async(
|
|
381
|
+
operation: Callable[[], T],
|
|
382
|
+
max_attempts: int = 3,
|
|
383
|
+
base_delay: float = 1.0,
|
|
384
|
+
operation_name: str = "operation",
|
|
385
|
+
) -> T:
|
|
386
|
+
"""Async version of retry_with_backoff for async operations.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
operation: Async function to retry (takes no arguments, returns awaitable)
|
|
390
|
+
max_attempts: Maximum number of attempts (default: 3)
|
|
391
|
+
base_delay: Base delay in seconds, doubles each retry (default: 1.0)
|
|
392
|
+
operation_name: Name for logging purposes (default: "operation")
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
Result of successful operation
|
|
396
|
+
|
|
397
|
+
Raises:
|
|
398
|
+
Exception: Last exception if all retries fail
|
|
399
|
+
|
|
400
|
+
Example:
|
|
401
|
+
>>> async def fetch_page():
|
|
402
|
+
... response = await client.get(url, timeout=30.0)
|
|
403
|
+
... response.raise_for_status()
|
|
404
|
+
... return response.text
|
|
405
|
+
>>> content = await retry_with_backoff_async(fetch_page, operation_name=f"fetch {url}")
|
|
406
|
+
"""
|
|
407
|
+
import asyncio
|
|
408
|
+
|
|
409
|
+
last_exception: Exception | None = None
|
|
410
|
+
|
|
411
|
+
for attempt in range(1, max_attempts + 1):
|
|
412
|
+
try:
|
|
413
|
+
return await operation()
|
|
414
|
+
except Exception as e:
|
|
415
|
+
last_exception = e
|
|
416
|
+
if attempt < max_attempts:
|
|
417
|
+
delay = base_delay * (2 ** (attempt - 1))
|
|
418
|
+
logger.warning(
|
|
419
|
+
"%s failed (attempt %d/%d), retrying in %.1fs: %s",
|
|
420
|
+
operation_name,
|
|
421
|
+
attempt,
|
|
422
|
+
max_attempts,
|
|
423
|
+
delay,
|
|
424
|
+
e,
|
|
425
|
+
)
|
|
426
|
+
await asyncio.sleep(delay)
|
|
427
|
+
else:
|
|
428
|
+
logger.error("%s failed after %d attempts: %s", operation_name, max_attempts, e)
|
|
429
|
+
|
|
430
|
+
if last_exception is not None:
|
|
431
|
+
raise last_exception
|
|
432
|
+
raise RuntimeError(f"{operation_name} failed with no exception captured")
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Skill Seekers MCP (Model Context Protocol) server package.
|
|
2
|
+
|
|
3
|
+
This package provides MCP server integration for Claude Code, allowing
|
|
4
|
+
natural language interaction with Skill Seekers tools.
|
|
5
|
+
|
|
6
|
+
Main modules:
|
|
7
|
+
- server_fastmcp: FastMCP-based server with 17 tools (MCP 2025 spec)
|
|
8
|
+
- agent_detector: AI coding agent detection and configuration
|
|
9
|
+
|
|
10
|
+
Available MCP Tools:
|
|
11
|
+
- list_configs: List all available preset configurations
|
|
12
|
+
- generate_config: Generate a new config file for any docs site
|
|
13
|
+
- validate_config: Validate a config file structure
|
|
14
|
+
- estimate_pages: Estimate page count before scraping
|
|
15
|
+
- scrape_docs: Scrape and build a skill
|
|
16
|
+
- package_skill: Package skill into .zip file (with auto-upload)
|
|
17
|
+
- upload_skill: Upload .zip to Claude
|
|
18
|
+
- split_config: Split large documentation configs
|
|
19
|
+
- generate_router: Generate router/hub skills
|
|
20
|
+
|
|
21
|
+
Agent Detection:
|
|
22
|
+
- Supports 5 AI coding agents: Claude Code, Cursor, Windsurf, VS Code + Cline, IntelliJ IDEA
|
|
23
|
+
- Auto-detects installed agents on Linux, macOS, and Windows
|
|
24
|
+
- Generates correct MCP config for each agent (stdio vs HTTP)
|
|
25
|
+
|
|
26
|
+
Usage:
|
|
27
|
+
The MCP server is typically run by Claude Code via configuration
|
|
28
|
+
in ~/.config/claude-code/mcp.json
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
__version__ = "2.7.2"
|
|
32
|
+
|
|
33
|
+
__all__ = ["agent_detector"]
|