
Skillsmp Searcher
- 36 installs
- 3 repo stars
- Updated February 8, 2026
- gccszs/skillsmp-searcher
Search and discover skills on the SkillsMP marketplace using either keyword search or AI semantic search from a natural-language description.
About
Provides keyword and semantic search over the SkillsMP skill marketplace, returning results that can be sorted by popularity or recency. A developer uses it to find skills on a topic or locate a skill from a plain-language description of what they need.
- Two modes: keyword search and AI semantic search
- Requires a configured API key stored in references/api_key.txt
Skillsmp Searcher by the numbers
- 36 all-time installs (skills.sh)
- Ranked #387 of 782 Skill Development skills by installs in the Skillselion catalog
- Data as of Jul 24, 2026 (Skillselion catalog sync)
npx skills add https://github.com/gccszs/skillsmp-searcher --skill skillsmp-searcherAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 36 |
|---|---|
| repo stars | ★ 3 |
| Last updated | February 8, 2026 |
| Repository | gccszs/skillsmp-searcher ↗ |
What it does
Search and discover skills on the SkillsMP marketplace using either keyword search or AI semantic search from a natural-language description.
Files
SkillsMP 技能搜索
此技能提供对 SkillsMP 技能商城的搜索功能,帮助用户快速发现和定位所需的技能。
API 配置
首次使用前,需要配置 API Key。API Key 存储在 references/api_key.txt 中。
格式:纯文本的 API Key 字符串(例如:sk_live_skillsmp_eb_6A4Y9LJAhtzPFsmX0v67zhingVC0CrQZ4Qqlin4)
注意:请确保 API Key 安全,不要将 SKILL.md 或包含 API Key 的文件提交到公共仓库。
搜索模式
1. 关键词搜索
使用 scripts/search_skills.py 进行基于关键词的搜索。
适用场景:
- 用户使用明确的关键词搜索(如 "SEO"、"PDF"、"翻译")
- 需要按热门度或最新时间排序
- 需要分页浏览结果
参数:
q(必需): 搜索关键词page: 页码,默认 1limit: 每页数量,默认 20,最大 100sortBy: 排序方式,stars(热门,默认)或recent(最新)
示例:
python scripts/search_skills.py "SEO" --page 1 --limit 10 --sortBy stars2. AI 语义搜索
使用 scripts/ai_search.py 进行基于语义理解的搜索。
适用场景:
- 用户使用自然语言描述需求(如"如何制作视频"、"帮我处理PDF文档")
- 搜索意图复杂,需要理解上下文
- 不确定具体关键词,希望AI智能匹配
参数:
q(必需): 自然语言搜索查询
示例:
python scripts/ai_search.py "How to create a web scraper"API 端点
详细的 API 文档请参考 references/api_documentation.md。
基础 URL: https://skillsmp.com/api/v1
| 端点 | 方法 | 功能 |
|---|---|---|
/skills/search | GET | 关键词搜索 |
/skills/ai-search | GET | AI 语义搜索 |
错误处理
API 错误码:
| 错误码 | HTTP状态 | 说明 |
|---|---|---|
MISSING_API_KEY | 401 | 未提供 API Key |
INVALID_API_KEY | 401 | API Key 无效 |
MISSING_QUERY | 400 | 缺少必需的查询参数 |
INTERNAL_ERROR | 500 | 服务器内部错误 |
错误响应格式:
{
"success": false,
"error": {
"code": "INVALID_API_KEY",
"message": "The provided API key is invalid"
}
}使用流程
1. 确保 API Key 已配置在 references/api_key.txt 2. 根据用户需求选择搜索模式:
- 明确关键词 → 关键词搜索
- 自然语言描述 → AI 语义搜索
3. 运行相应的脚本获取结果 4. 解析并展示搜索结果给用户
# Environment Variables Example / 环境变量示例文件
# 环境变量示例文件
#
# Copy this file to .env (add .env to .gitignore) and fill in your actual values
# 复制此文件为 .env(将 .env 添加到 .gitignore)并填入您的实际值
# SkillsMP API Key
# Get your API key from: https://skillsmp.com/
# 从以下地址获取您的API密钥:https://skillsmp.com/
SKILLSMP_API_KEY=sk_live_skillsmp_your_api_key_here
# Optional: Custom API base URL (for testing or enterprise deployments)
# 可选:自定义API基础URL(用于测试或企业部署)
# SKILLSMP_API_BASE_URL=https://skillsmp.com/api/v1
name: CD
on:
push:
branches: [ main ]
workflow_dispatch:
jobs:
package:
name: Package and Release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install requests
- name: Validate skill structure
run: |
if [ ! -f "skills/skillsmp-searcher/SKILL.md" ]; then
echo "Error: SKILL.md not found"
exit 1
fi
if [ ! -d "skills/skillsmp-searcher/scripts" ]; then
echo "Error: scripts directory not found"
exit 1
fi
echo "Skill structure validation passed"
- name: Package skill
run: |
cd skills/skillsmp-searcher
zip -r ../../skillsmp-searcher.skill . -x "*.git*" "*.pyc" "__pycache__/*"
- name: Generate release notes
id: release_notes
run: |
echo "## 🚀 Automated Release" > release_notes.md
echo "" >> release_notes.md
echo "This is an automated release from the main branch." >> release_notes.md
echo "" >> release_notes.md
echo "### Changes" >> release_notes.md
echo "- Latest changes from the main branch" >> release_notes.md
echo "" >> release_notes.md
echo "### Installation" >> release_notes.md
echo '```bash' >> release_notes.md
echo "claude skill install skillsmp-searcher.skill" >> release_notes.md
echo '```' >> release_notes.md
- name: Create Release
uses: softprops/action-gh-release@v1
with:
name: Latest Release
body_path: release_notes.md
files: skillsmp-searcher.skill
tag_name: latest
draft: false
prerelease: false
fail_on_unmatched_files: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
name: CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
lint:
name: Code Quality
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install linting tools
run: |
python -m pip install --upgrade pip
pip install black flake8 isort mypy types-requests
- name: Check code formatting with Black
run: black --check skills/skillsmp-searcher/scripts/
- name: Check code style with Flake8
run: |
flake8 skills/skillsmp-searcher/scripts/ --count --select=E9,F63,F7,F82 --show-source --statistics
flake8 skills/skillsmp-searcher/scripts/ --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Check import sorting with isort
run: isort --check-only skills/skillsmp-searcher/scripts/
- name: Type checking with mypy
run: mypy skills/skillsmp-searcher/scripts/ --ignore-missing-imports
test:
name: Test (Python ${{ matrix.python }}, ${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python: ['3.9', '3.10', '3.11', '3.12']
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest pytest-mock requests
- name: Run tests
run: pytest tests/ -v
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
*.manifest
*.spec
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# IDEs
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Skills specific
*.skill
# API keys (keep template, ignore real keys if created separately)
skills/skillsmp-searcher/references/api_key_real.txt
# Worktrees
.worktrees
worktrees
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
[tool.black]
line-length = 100
target-version = ['py39', 'py310', 'py311', 'py312']
include = '\.pyi?$'
[tool.isort]
profile = "black"
line_length = 100
multi_line_output = 3
include_trailing_comma = true
force_grid_wrap = 0
use_parentheses = true
ensure_newline_before_comments = true
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = --verbose
markers =
unit: Unit tests
integration: Integration tests
SkillsMP Searcher
English | 简体中文
---
SkillsMP Searcher 是一个 Claude Code 技能,为 SkillsMP 技能商城提供强大的搜索功能。它支持关键词搜索和AI驱动的语义搜索,帮助您快速发现和安装有用的技能。
功能特性
- 关键词搜索: 通过特定关键词搜索技能,支持分页和排序
- AI语义搜索: 使用自然语言查询查找相关技能,由Cloudflare AI驱动
- 跨平台: 支持Windows、macOS和Linux
- Python 3.9+: 支持Python 3.9、3.10、3.11和3.12
- 安全的API密钥管理: 多种配置方式和安全最佳实践
- 一键安装: 直接从搜索结果安装技能
- 更新检查: 自动检查已安装技能的更新
安装
选择以下任一方法安装 SkillsMP Searcher:
方法1:NPX 快速安装 ⚡(推荐)
最快的安装方式,直接从 GitHub 安装:
npx skills add gccszs/skillsmp-searcher这将自动下载并安装最新版本的技能。
方法2:从发布文件安装
1. 从发布页面下载最新的 skillsmp-searcher.skill 2. 使用 Claude Code CLI 安装:
claude skill install skillsmp-searcher.skill方法3:从 GitHub 安装
# 克隆仓库
git clone https://github.com/gccszs/skillsmp-searcher.git
# 从本地目录安装
claude skill install skillsmp-searcher/skills/skillsmp-searcher方法4:一行命令安装(PowerShell)
# 下载并安装,一条命令完成
Invoke-WebRequest -Uri "https://github.com/gccszs/skillsmp-searcher/releases/latest/download/skillsmp-searcher.skill" -OutFile "skillsmp-searcher.skill"; claude skill install skillsmp-searcher.skill方法5:一行命令安装(Bash)
# 下载并安装,一条命令完成
curl -L https://github.com/gccszs/skillsmp-searcher/releases/latest/download/skillsmp-searcher.skill -o skillsmp-searcher.skill && claude skill install skillsmp-searcher.skill验证安装
claude skill list您应该能在已安装技能列表中看到 skillsmp-searcher。
配置
🔑 API密钥设置
使用此技能前,需要配置您的SkillsMP API密钥。选择以下任一方法:
方法1:环境变量(推荐)✅
# Linux/macOS - 添加到 ~/.bashrc 或 ~/.zshrc
export SKILLSMP_API_KEY="sk_live_skillsmp_您的实际密钥"
# Windows PowerShell
[System.Environment]::SetEnvironmentVariable('SKILLSMP_API_KEY', 'sk_live_skillsmp_您的实际密钥', 'User')方法2:配置文件(用于开发)
# 创建文件:skills/skillsmp-searcher/references/api_key_real.txt
# 粘贴您的API密钥(仅密钥本身,不要有其他内容)
sk_live_skillsmp_您的实际密钥方法3:命令行参数(一次性使用)
python skills/skillsmp-searcher/scripts/search_skills.py "SEO" --api-key "您的密钥"⚠️ 安全最佳实践
- 永远不要将API密钥提交到版本控制系统
- 使用环境变量进行生产部署
- 密钥泄露后立即轮换,访问SkillsMP控制台
- 监控API使用情况,发现异常活动
💡 提示:将.env.example复制为.env并填入您的API密钥用于本地开发。.env文件会自动被git忽略。
使用方法
关键词搜索
使用特定关键词搜索技能:
python skills/skillsmp-searcher/scripts/search_skills.py "SEO" --limit 10 --sort stars参数:
query: 搜索关键词(必需)--page: 页码(默认:1)--limit: 每页项目数(默认:20,最大:100)--sort: 按stars(默认)或recent排序
AI语义搜索
使用自然语言搜索:
python skills/skillsmp-searcher/scripts/ai_search.py "如何创建网络爬虫"一键安装技能 🔧
直接从搜索结果安装技能:
# 搜索并安装第一个结果
python skills/skillsmp-searcher/scripts/install_skill.py install "视频编辑"
# 搜索并按索引安装
python skills/skillsmp-searcher/scripts/install_skill.py install "PDF" --index 2
# 从直接URL安装
python skills/skillsmp-searcher/scripts/install_skill.py install "https://github.com/user/repo/releases/latest/download/skill.skill"
# 列出已安装的技能
python skills/skillsmp-searcher/scripts/install_skill.py list安装选项:
query: 搜索查询或.skill文件的直接URL/路径--index N: 安装搜索结果中的第N个技能(默认:1)--page N: 搜索页码(默认:1)--sort: 按stars(默认)或recent排序
查看技能详情 ℹ️
获取特定技能的详细信息:
python skills/skillsmp-searcher/scripts/skill_info.py "技能名称"详情包括:
- 作者和星标数
- 版本信息
- 完整描述
- 标签和分类
- 安装命令
- 使用示例
检查技能更新 🔄
检查所有已安装技能的可用更新:
# 检查更新(遵守1小时缓存)
python skills/skillsmp-searcher/scripts/check_updates.py
# 强制检查,即使最近检查过
python skills/skillsmp-searcher/scripts/check_updates.py --force
# 以JSON格式输出
python skills/skillsmp-searcher/scripts/check_updates.py --json功能:
- 检查所有已安装技能与SkillsMP商城的对比
- 智能缓存(最多每小时检查一次)
- 显示当前版本与最新版本
- 一行更新命令
API文档
- 官方API文档: https://skillsmp.com/docs/api
- 中文API文档: https://skillsmp.com/zh/docs/api
- 本地参考文档:
skills/skillsmp-searcher/references/api_documentation.md
开发
运行测试
# 安装依赖
pip install -r requirements.txt
# 运行测试
pytest
# 运行测试并生成覆盖率报告
pytest --cov=scripts代码质量检查
# 格式化代码
black scripts/
# 检查代码风格
flake8 scripts/
# 类型检查
mypy scripts/项目结构
skillsmp-searcher/
├── .github/
│ └── workflows/ # CI/CD工作流
├── skills/
│ └── skillsmp-searcher/ # Skill包
│ ├── SKILL.md # Skill元数据
│ ├── scripts/ # 可执行脚本
│ ├── references/ # 文档和配置
│ └── assets/ # 资源文件
├── tests/ # 测试套件
├── requirements.txt # Python依赖
└── README.md # 本文件贡献
欢迎贡献!请随时提交Pull Request。
许可证
本项目采用MIT许可证 - 详见LICENSE文件。
相关链接
SkillsMP Searcher
English | 简体中文
---
SkillsMP Searcher is a Claude Code skill that enables powerful search capabilities for the SkillsMP skill marketplace. It provides both keyword-based search and AI-powered semantic search to help you quickly discover and install useful skills.
Features
- Keyword Search: Search skills by specific keywords with pagination and sorting options
- AI Semantic Search: Use natural language queries to find relevant skills powered by Cloudflare AI
- Cross-Platform: Works on Windows, macOS, and Linux
- Python 3.9+: Supports Python 3.9, 3.10, 3.11, and 3.12
- Secure API Key Management: Multiple configuration methods with security best practices
- One-Click Installation: Install skills directly from search results
- Update Checker: Automatically check for updates to installed skills
Installation
Choose one of the following methods to install SkillsMP Searcher:
Method 1: NPX Quick Install (Recommended)
The fastest way to install directly from GitHub:
npx skills add gccszs/skillsmp-searcherThis will automatically download and install the latest version of the skill.
Method 2: Install from Release File
1. Download the latest skillsmp-searcher.skill from Releases 2. Install using Claude Code CLI:
claude skill install skillsmp-searcher.skillMethod 3: Install from GitHub
# Clone the repository
git clone https://github.com/gccszs/skillsmp-searcher.git
# Install from local directory
claude skill install skillsmp-searcher/skills/skillsmp-searcherMethod 4: One-Line Install (PowerShell)
# Download and install in one command
Invoke-WebRequest -Uri "https://github.com/gccszs/skillsmp-searcher/releases/latest/download/skillsmp-searcher.skill" -OutFile "skillsmp-searcher.skill"; claude skill install skillsmp-searcher.skillMethod 5: One-Line Install (Bash)
# Download and install in one command
curl -L https://github.com/gccszs/skillsmp-searcher/releases/latest/download/skillsmp-searcher.skill -o skillsmp-searcher.skill && claude skill install skillsmp-searcher.skillVerify Installation
claude skill listYou should see skillsmp-searcher in the list of installed skills.
Configuration
API Key Setup
Before using this skill, you need to configure your SkillsMP API key. Choose one of the following methods:
Method 1: Environment Variable (Recommended)
# Linux/macOS - Add to ~/.bashrc or ~/.zshrc
export SKILLSMP_API_KEY="sk_live_skillsmp_your_actual_key_here"
# Windows PowerShell
[System.Environment]::SetEnvironmentVariable('SKILLSMP_API_KEY', 'sk_live_skillsmp_your_actual_key_here', 'User')Method 2: Configuration File (For Development)
# Create file: skills/skillsmp-searcher/references/api_key_real.txt
# Paste your API key (only the key, nothing else)
sk_live_skillsmp_your_actual_key_hereMethod 3: Command-Line Argument (One-Time Use)
python skills/skillsmp-searcher/scripts/search_skills.py "SEO" --api-key "your_key_here"Security Best Practices
- Never commit API keys to version control
- Use environment variables for production deployments
- Rotate compromised keys immediately at SkillsMP Dashboard
- Monitor API usage for unusual activity
Tip: Copy.env.exampleto.envand fill in your API key for local development. The.envfile is automatically gitignored.
Usage
Keyword Search
Search for skills using specific keywords:
python skills/skillsmp-searcher/scripts/search_skills.py "SEO" --limit 10 --sort starsParameters:
query: Search keyword (required)--page: Page number (default: 1)--limit: Items per page (default: 20, max: 100)--sort: Sort bystars(default) orrecent
AI Semantic Search
Search using natural language:
python skills/skillsmp-searcher/scripts/ai_search.py "How to create a web scraper"One-Click Skill Installation
Install skills directly from search results:
# Search and install the first result
python skills/skillsmp-searcher/scripts/install_skill.py install "video editing"
# Search and install by index
python skills/skillsmp-searcher/scripts/install_skill.py install "PDF" --index 2
# Install from direct URL
python skills/skillsmp-searcher/scripts/install_skill.py install "https://github.com/user/repo/releases/latest/download/skill.skill"
# List installed skills
python skills/skillsmp-searcher/scripts/install_skill.py listInstallation Options:
query: Search query or direct URL/path to.skillfile--index N: Install the Nth skill from search results (default: 1)--page N: Search page number (default: 1)--sort: Sort bystars(default) orrecent
View Skill Details
Get detailed information about a specific skill:
python skills/skillsmp-searcher/scripts/skill_info.py "skill-name"Details include:
- Author and stars
- Version information
- Full description
- Tags and categories
- Installation commands
- Usage examples
Check for Updates
Check all installed skills for available updates:
# Check for updates (respects 1-hour cache)
python skills/skillsmp-searcher/scripts/check_updates.py
# Force check even if recently checked
python skills/skillsmp-searcher/scripts/check_updates.py --force
# Output in JSON format
python skills/skillsmp-searcher/scripts/check_updates.py --jsonFeatures:
- Checks all installed skills against SkillsMP marketplace
- Smart caching (checks at most once per hour)
- Shows current vs latest version
- One-line update commands
API Documentation
- Official API Documentation: https://skillsmp.com/docs/api
- Local Reference:
skills/skillsmp-searcher/references/api_documentation.md
Development
Running Tests
# Install dependencies
pip install -r requirements.txt
# Run tests
pytest
# Run tests with coverage
pytest --cov=scriptsCode Quality
# Format code
black scripts/
# Check code style
flake8 scripts/
# Type checking
mypy scripts/Project Structure
skillsmp-searcher/
├── .github/
│ └── workflows/ # CI/CD workflows
├── skills/
│ └── skillsmp-searcher/ # Skill package
│ ├── SKILL.md # Skill metadata
│ ├── scripts/ # Executable scripts
│ ├── references/ # Documentation and configs
│ └── assets/ # Resource files
├── tests/ # Test suite
├── requirements.txt # Python dependencies
└── README.md # This fileContributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Links
SkillsMP API Documentation
Base URL
https://skillsmp.com/api/v1Authentication
All API requests require authentication using an API Key via Bearer token.
Header: Authorization: Bearer <api_key>
Example API Key: sk_live_skillsmp_eb_6A4Y9LJAhtzPFsmX0v67zhingVC0CrQZ4Qqlin4
Key Management:
- Regenerate Key: Click "Regenerate Key" button in the dashboard
- Delete Key: Click "Delete Key" button in the dashboard
- Usage Tracking: Monitor creation date and last used timestamp
Endpoints
1. Keyword Search
Search skills using keywords.
Endpoint: GET /skills/search
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| q | string | Yes | Search query (keyword) |
| page | number | No | Page number (default: 1) |
| limit | number | No | Items per page (default: 20, max: 100) |
| sortBy | string | No | Sort by: stars (default) or recent |
Example Request:
curl -X GET "https://skillsmp.com/api/v1/skills/search?q=SEO" \
-H "Authorization: Bearer YOUR_API_KEY"Example Request with Pagination:
curl -X GET "https://skillsmp.com/api/v1/skills/search?q=SEO&page=1&limit=10&sortBy=stars" \
-H "Authorization: Bearer YOUR_API_KEY"2. AI Semantic Search
AI-powered semantic search using Cloudflare AI.
Endpoint: GET /skills/ai-search
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| q | string | Yes | Natural language search query |
Example Request:
curl -X GET "https://skillsmp.com/api/v1/skills/ai-search?q=How+to+create+a+web+scraper" \
-H "Authorization: Bearer YOUR_API_KEY"Response Format
Success Response
{
"success": true,
"data": {
"skills": [
{
"id": "skill_id",
"name": "Skill Name",
"description": "Skill description...",
"author": "Author Name",
"stars": 42,
"relevance_score": 0.95
}
],
"total": 100,
"page": 1,
"limit": 20
}
}Error Response
{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "Human-readable error message"
}
}Error Codes
| Error Code | HTTP Status | Description |
|---|---|---|
| MISSING_API_KEY | 401 | API key not provided |
| INVALID_API_KEY | 401 | The provided API key is invalid |
| MISSING_QUERY | 400 | Missing required query parameter |
| INTERNAL_ERROR | 500 | Internal server error |
Rate Limiting
Please refer to the SkillsMP dashboard for current rate limits and usage statistics.
SDK Examples
Python
import requests
def search_skills(query, api_key):
url = "https://skillsmp.com/api/v1/skills/search"
headers = {"Authorization": f"Bearer {api_key}"}
params = {"q": query}
response = requests.get(url, headers=headers, params=params)
return response.json()
# Usage
results = search_skills("SEO", "your_api_key_here")JavaScript
async function searchSkills(query, apiKey) {
const url = new URL('https://skillsmp.com/api/v1/skills/search');
url.searchParams.append('q', query);
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${apiKey}`
}
});
return await response.json();
}
// Usage
const results = await searchSkills('SEO', 'your_api_key_here');# SkillsMP API Key
# Get your API key from: https://skillsmp.com/
# Replace the text below with your actual API key
sk_live_skillsmp_your_api_key_here
# Core dependencies
requests>=2.31.0
# Testing
pytest>=7.4.0
pytest-cov>=4.1.0
pytest-mock>=3.11.0
# Code quality
black>=23.7.0
flake8>=6.1.0
isort>=5.12.0
mypy>=1.5.0
# Type stubs
types-requests>=2.31.0
#!/usr/bin/env python3
"""
SkillsMP AI Semantic Search Script
Search for skills using AI-powered semantic search on SkillsMP marketplace.
"""
import requests
import argparse
import json
import sys
import os
# API Configuration
BASE_URL = "https://skillsmp.com/api/v1"
API_KEY_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), "references", "api_key.txt")
def load_api_key():
"""Load API key from references/api_key.txt"""
try:
with open(API_KEY_FILE, 'r') as f:
api_key = f.read().strip()
if not api_key:
raise ValueError("API key file is empty")
return api_key
except FileNotFoundError:
print(f"Error: API key file not found at {API_KEY_FILE}")
print("Please create the file and add your SkillsMP API key.")
sys.exit(1)
except Exception as e:
print(f"Error loading API key: {e}")
sys.exit(1)
def ai_search(query, api_key=None):
"""
Search skills using AI semantic search.
Args:
query: Natural language search query
api_key: SkillsMP API key
Returns:
dict: Search results
"""
if api_key is None:
api_key = load_api_key()
url = f"{BASE_URL}/skills/ai-search"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
params = {
"q": query
}
try:
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
return response.json()
except requests.exceptions.HTTPError as e:
print(f"HTTP Error: {e}")
if response.status_code == 401:
error_data = response.json()
print(f"API Error: {error_data.get('error', {}).get('message', 'Authentication failed')}")
sys.exit(1)
except requests.exceptions.RequestException as e:
print(f"Request Error: {e}")
sys.exit(1)
def format_results(results):
"""Format search results for display"""
if not results.get("success", True):
error = results.get("error", {})
print(f"Error: {error.get('code', 'UNKNOWN')} - {error.get('message', 'Unknown error')}")
return
data = results.get("data", {})
skills = data.get("skills", [])
print(f"\n=== AI Search Results ===\n")
if not skills:
print("No skills found matching your query.")
return
for i, skill in enumerate(skills, 1):
name = skill.get("name", "Unknown")
description = skill.get("description", "No description")
relevance = skill.get("relevance_score", 0)
stars = skill.get("stars", 0)
author = skill.get("author", "Unknown")
print(f"{i}. {name}")
print(f" Author: {author} | Stars: {stars} | Relevance: {relevance:.2f}")
print(f" Description: {description[:100]}{'...' if len(description) > 100 else ''}")
print()
def main():
parser = argparse.ArgumentParser(description="AI-powered semantic search on SkillsMP marketplace")
parser.add_argument("query", help="Natural language search query")
parser.add_argument("--json", action="store_true", help="Output raw JSON")
parser.add_argument("--api-key", help="API key (overrides file)")
args = parser.parse_args()
results = ai_search(
query=args.query,
api_key=args.api_key
)
if args.json:
print(json.dumps(results, indent=2))
else:
format_results(results)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
SkillsMP Keyword Search Script
Search for skills using keywords on SkillsMP marketplace.
"""
import requests
import argparse
import json
import sys
import os
# API Configuration
BASE_URL = "https://skillsmp.com/api/v1"
API_KEY_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), "references", "api_key.txt")
def load_api_key():
"""Load API key from references/api_key.txt"""
try:
with open(API_KEY_FILE, 'r') as f:
api_key = f.read().strip()
if not api_key:
raise ValueError("API key file is empty")
return api_key
except FileNotFoundError:
print(f"Error: API key file not found at {API_KEY_FILE}")
print("Please create the file and add your SkillsMP API key.")
sys.exit(1)
except Exception as e:
print(f"Error loading API key: {e}")
sys.exit(1)
def search_skills(query, page=1, limit=20, sort_by="stars", api_key=None):
"""
Search skills using keyword search.
Args:
query: Search keyword
page: Page number (default: 1)
limit: Items per page (default: 20, max: 100)
sort_by: Sort by 'stars' or 'recent' (default: 'stars')
api_key: SkillsMP API key
Returns:
dict: Search results
"""
if api_key is None:
api_key = load_api_key()
url = f"{BASE_URL}/skills/search"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
params = {
"q": query,
"page": page,
"limit": min(limit, 100),
"sortBy": sort_by
}
try:
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
return response.json()
except requests.exceptions.HTTPError as e:
print(f"HTTP Error: {e}")
if response.status_code == 401:
error_data = response.json()
print(f"API Error: {error_data.get('error', {}).get('message', 'Authentication failed')}")
sys.exit(1)
except requests.exceptions.RequestException as e:
print(f"Request Error: {e}")
sys.exit(1)
def format_results(results):
"""Format search results for display"""
if not results.get("success", True):
error = results.get("error", {})
print(f"Error: {error.get('code', 'UNKNOWN')} - {error.get('message', 'Unknown error')}")
return
data = results.get("data", {})
skills = data.get("skills", [])
total = data.get("total", 0)
print(f"\n=== Search Results ===")
print(f"Total: {total} skills found\n")
for i, skill in enumerate(skills, 1):
name = skill.get("name", "Unknown")
description = skill.get("description", "No description")
stars = skill.get("stars", 0)
author = skill.get("author", "Unknown")
print(f"{i}. {name}")
print(f" Author: {author} | Stars: {stars}")
print(f" Description: {description[:100]}{'...' if len(description) > 100 else ''}")
print()
def main():
parser = argparse.ArgumentParser(description="Search SkillsMP marketplace for skills")
parser.add_argument("query", help="Search keyword")
parser.add_argument("--page", type=int, default=1, help="Page number (default: 1)")
parser.add_argument("--limit", type=int, default=20, help="Items per page (default: 20, max: 100)")
parser.add_argument("--sort", choices=["stars", "recent"], default="stars",
help="Sort by: 'stars' (default) or 'recent'")
parser.add_argument("--json", action="store_true", help="Output raw JSON")
parser.add_argument("--api-key", help="API key (overrides file)")
args = parser.parse_args()
results = search_skills(
query=args.query,
page=args.page,
limit=args.limit,
sort_by=args.sort,
api_key=args.api_key
)
if args.json:
print(json.dumps(results, indent=2))
else:
format_results(results)
if __name__ == "__main__":
main()
# Worktrees directory
.worktrees/
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
skillsmp-searcher
skillsmp开源网站配套得检索agent-skills,只需要配合网站自带的API key,让Agent自主帮助你检索你想要的技能
SkillsMP API Documentation
Base URL
https://skillsmp.com/api/v1Authentication
All API requests require authentication using an API Key via Bearer token.
Header: Authorization: Bearer <api_key>
Example API Key: sk_live_skillsmp_eb_6A4Y9LJAhtzPFsmX0v67zhingVC0CrQZ4Qqlin4
Key Management:
- Regenerate Key: Click "Regenerate Key" button in the dashboard
- Delete Key: Click "Delete Key" button in the dashboard
- Usage Tracking: Monitor creation date and last used timestamp
Endpoints
1. Keyword Search
Search skills using keywords.
Endpoint: GET /skills/search
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| q | string | Yes | Search query (keyword) |
| page | number | No | Page number (default: 1) |
| limit | number | No | Items per page (default: 20, max: 100) |
| sortBy | string | No | Sort by: stars (default) or recent |
Example Request:
curl -X GET "https://skillsmp.com/api/v1/skills/search?q=SEO" \
-H "Authorization: Bearer YOUR_API_KEY"Example Request with Pagination:
curl -X GET "https://skillsmp.com/api/v1/skills/search?q=SEO&page=1&limit=10&sortBy=stars" \
-H "Authorization: Bearer YOUR_API_KEY"2. AI Semantic Search
AI-powered semantic search using Cloudflare AI.
Endpoint: GET /skills/ai-search
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| q | string | Yes | Natural language search query |
Example Request:
curl -X GET "https://skillsmp.com/api/v1/skills/ai-search?q=How+to+create+a+web+scraper" \
-H "Authorization: Bearer YOUR_API_KEY"Response Format
Success Response
{
"success": true,
"data": {
"skills": [
{
"id": "skill_id",
"name": "Skill Name",
"description": "Skill description...",
"author": "Author Name",
"stars": 42,
"relevance_score": 0.95
}
],
"total": 100,
"page": 1,
"limit": 20
}
}Error Response
{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "Human-readable error message"
}
}Error Codes
| Error Code | HTTP Status | Description |
|---|---|---|
| MISSING_API_KEY | 401 | API key not provided |
| INVALID_API_KEY | 401 | The provided API key is invalid |
| MISSING_QUERY | 400 | Missing required query parameter |
| INTERNAL_ERROR | 500 | Internal server error |
Rate Limiting
Please refer to the SkillsMP dashboard for current rate limits and usage statistics.
SDK Examples
Python
import requests
def search_skills(query, api_key):
url = "https://skillsmp.com/api/v1/skills/search"
headers = {"Authorization": f"Bearer {api_key}"}
params = {"q": query}
response = requests.get(url, headers=headers, params=params)
return response.json()
# Usage
results = search_skills("SEO", "your_api_key_here")JavaScript
async function searchSkills(query, apiKey) {
const url = new URL('https://skillsmp.com/api/v1/skills/search');
url.searchParams.append('q', query);
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${apiKey}`
}
});
return await response.json();
}
// Usage
const results = await searchSkills('SEO', 'your_api_key_here');# SkillsMP API Key
# Get your API key from: https://skillsmp.com/
# Replace the text below with your actual API key
sk_live_skillsmp_your_api_key_here
#!/usr/bin/env python3
"""
SkillsMP AI Semantic Search Script
Search for skills using AI-powered semantic search on SkillsMP marketplace.
"""
import argparse
import json
import sys
from typing import Optional
from utils import APIRequestError, SkillsMPError, make_api_request
def ai_search(query: str, api_key: Optional[str] = None) -> dict:
"""
Search skills using AI semantic search.
Args:
query: Natural language search query
api_key: SkillsMP API key
Returns:
dict: Search results
Raises:
SkillsMPError: If the search fails
"""
params = {"q": query}
return make_api_request("/skills/ai-search", params, api_key=api_key)
def format_results(results):
"""Format search results for display"""
if not results.get("success", True):
error = results.get("error", {})
print(f"Error: {error.get('code', 'UNKNOWN')} - {error.get('message', 'Unknown error')}")
return
data = results.get("data", {})
skills = data.get("skills", [])
print(f"\n=== AI Search Results ===\n")
if not skills:
print("No skills found matching your query.")
return
for i, skill in enumerate(skills, 1):
name = skill.get("name", "Unknown")
description = skill.get("description", "No description")
relevance = skill.get("relevance_score", 0)
stars = skill.get("stars", 0)
author = skill.get("author", "Unknown")
print(f"{i}. {name}")
print(f" Author: {author} | Stars: {stars} | Relevance: {relevance:.2f}")
print(f" Description: {description[:100]}{'...' if len(description) > 100 else ''}")
print()
def main():
parser = argparse.ArgumentParser(
description="AI-powered semantic search on SkillsMP marketplace"
)
parser.add_argument("query", help="Natural language search query")
parser.add_argument("--json", action="store_true", help="Output raw JSON")
parser.add_argument("--api-key", help="API key (overrides file)")
args = parser.parse_args()
try:
results = ai_search(query=args.query, api_key=args.api_key)
if args.json:
print(json.dumps(results, indent=2))
else:
format_results(results)
except SkillsMPError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
SkillsMP Skill Update Checker
Check for updates to installed skills by comparing local file times with SkillsMP API.
"""
import argparse
import json
import re
import sys
import time
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional
from utils import (
APIRequestError,
SkillsMPError,
get_claude_skills_dir,
load_api_key,
make_api_request,
)
def get_skill_name_from_md(skill_md_path: Path) -> Optional[str]:
"""
Extract skill name from SKILL.md frontmatter.
Args:
skill_md_path: Path to SKILL.md file
Returns:
Skill name or None if not found
"""
try:
with open(skill_md_path, "r", encoding="utf-8") as f:
content = f.read()
# Extract name from YAML frontmatter
match = re.search(r"^name:\s*(.+)$", content, re.MULTILINE)
if match:
return match.group(1).strip()
except Exception:
pass
return None
def get_installed_skills_with_metadata(skills_dir: Path) -> List[Dict]:
"""
Scan local skills directory and extract metadata.
Args:
skills_dir: Path to Claude skills directory
Returns:
List of dicts with skill metadata: name, path, local_modified
"""
skills: List[Dict[str, Any]] = []
if not skills_dir.exists():
return skills
for skill_dir in skills_dir.iterdir():
if not skill_dir.is_dir():
continue
skill_md = skill_dir / "SKILL.md"
if not skill_md.exists():
continue
skill_name = get_skill_name_from_md(skill_md)
if not skill_name:
continue
# Get directory modification time as Unix timestamp
local_mtime = skill_dir.stat().st_mtime
skills.append(
{
"name": skill_name,
"path": skill_dir,
"local_modified": local_mtime,
"local_modified_date": datetime.fromtimestamp(local_mtime).strftime("%Y-%m-%d"),
}
)
return skills
def search_skill_on_skillsmp(skill_name: str, api_key: Optional[str] = None) -> Optional[Dict]:
"""
Search for a skill on SkillsMP marketplace.
Args:
skill_name: Name of the skill to search for
api_key: SkillsMP API key
Returns:
Skill data from API or None if not found
"""
try:
params = {"q": skill_name, "limit": 5, "sortBy": "stars"}
result = make_api_request("/skills/search", params, api_key=api_key)
if not result.get("success"):
return None
skills_list = result.get("data", {}).get("skills", [])
if not skills_list:
return None
# Find exact match by name
for skill in skills_list:
if skill.get("name", "").lower() == skill_name.lower():
return skill
# If no exact match, return first result (best match)
return skills_list[0]
except (APIRequestError, SkillsMPError):
return None
def format_timestamp(unix_timestamp: int) -> str:
"""Convert Unix timestamp to readable date."""
return datetime.fromtimestamp(unix_timestamp).strftime("%Y-%m-%d")
def check_skill_updates(skills_dir: Optional[Path] = None, api_key: Optional[str] = None) -> Dict:
"""
Check all installed skills for available updates.
Args:
skills_dir: Custom skills directory
api_key: API key for SkillsMP API
Returns:
Dict with 'updates', 'up_to_date', 'not_found', 'errors' lists
"""
if skills_dir is None:
skills_dir = get_claude_skills_dir()
installed_skills = get_installed_skills_with_metadata(skills_dir)
result: Dict[str, List[Any]] = {
"updates": [], # Skills with remote updates
"up_to_date": [], # Skills that are current
"not_found": [], # Skills not found on SkillsMP
"errors": [], # Skills that had errors
}
if not installed_skills:
return result
print(f"🔍 Checking {len(installed_skills)} installed skills for updates...\n")
for i, skill in enumerate(installed_skills, 1):
skill_name = skill["name"]
local_mtime = skill["local_modified"]
# Show progress
print(f"[{i}/{len(installed_skills)}] Checking {skill_name}...", end=" ")
# Search on SkillsMP
remote_skill = search_skill_on_skillsmp(skill_name, api_key=api_key)
if not remote_skill:
print("❓ Not found on SkillsMP")
result["not_found"].append(skill)
# Add small delay to avoid rate limiting
time.sleep(0.3)
continue
# Extract remote data
remote_updated = remote_skill.get("updatedAt", 0)
remote_updated_date = format_timestamp(remote_updated)
github_url = remote_skill.get("githubUrl", "")
skill_url = remote_skill.get("skillUrl", "")
stars = remote_skill.get("stars", 0)
# Compare timestamps (allow 1 second tolerance for file system precision)
if remote_updated > local_mtime + 1:
print(f"⚠️ Update available!")
print(f" Local: {skill['local_modified_date']} | Remote: {remote_updated_date}")
result["updates"].append(
{
"name": skill_name,
"local_date": skill["local_modified_date"],
"remote_date": remote_updated_date,
"local_timestamp": local_mtime,
"remote_timestamp": remote_updated,
"github_url": github_url,
"skill_url": skill_url,
"stars": stars,
}
)
else:
print("✅ Up to date")
result["up_to_date"].append(skill)
# Add small delay to avoid rate limiting
time.sleep(0.3)
return result
def format_update_summary(result: Dict):
"""Format and display update check results."""
updates = result["updates"]
up_to_date = result["up_to_date"]
not_found = result["not_found"]
errors = result["errors"]
print("\n" + "=" * 60)
print("📊 UPDATE CHECK SUMMARY")
print("=" * 60)
# Show updates
if updates:
print(f"\n⚠️ {len(updates)} skill(s) with potential updates:\n")
for i, update in enumerate(updates, 1):
print(f"{i}. {update['name']}")
print(f" Local: {update['local_date']} | SkillsMP: {update['remote_date']}")
print(f" Stars: {update['stars']}")
print(f" GitHub: {update['github_url']}")
print()
else:
print("\n✨ No updates found - all checked skills are up to date!\n")
# Show not found
if not_found:
print(f"❓ {len(not_found)} skill(s) not found on SkillsMP:")
for skill in not_found:
print(f" - {skill['name']}")
print()
# Show errors
if errors:
print(f"❌ {len(errors)} skill(s) had errors:")
for error in errors:
print(f" - {error['name']}: {error['error']}")
print()
# Show summary
total_checked = len(updates) + len(up_to_date)
print(f"Total checked: {total_checked}")
print(f"Up to date: {len(up_to_date)}")
print(f"Updates available: {len(updates)}")
print(f"Not found: {len(not_found)}")
print(f"Errors: {len(errors)}")
def interactive_details_loop(result: Dict, api_key: Optional[str] = None):
"""
Interactive loop for viewing skill details and updating.
Args:
result: Update check result from check_skill_updates()
api_key: API key for API requests
"""
updates = result["updates"]
if not updates:
return
while True:
print("\n" + "=" * 60)
print(
"View details? Enter skill number (1-{}) or 'q' to quit:".format(len(updates)), end=" "
)
try:
user_input = input().strip()
if user_input.lower() == "q":
print("👋 Exiting update checker.")
break
skill_index = int(user_input)
if skill_index < 1 or skill_index > len(updates):
print(f"❌ Invalid number. Please enter 1-{len(updates)} or 'q'")
continue
# Get selected update
selected = updates[skill_index - 1]
skill_name = selected["name"]
github_url = selected["github_url"]
# Import skill_diff to show details
import subprocess
print(f"\n🔍 Fetching details for {skill_name}...")
subprocess.run(
["python", "skill_diff.py", skill_name],
cwd=Path(__file__).parent,
capture_output=False,
)
# Ask if user wants to update
print("\n" + "=" * 60)
print("Update this skill? [Y/n]:", end=" ")
update_choice = input().strip().lower()
if update_choice == "n":
print("⏭️ Skipped")
continue
if update_choice in ("", "y", "yes"):
# Import and run skill_downloader
print(f"\n📦 Updating {skill_name}...")
update_result = subprocess.run(
["python", "skill_downloader.py", skill_name, "--github-url", github_url],
cwd=Path(__file__).parent,
capture_output=False,
)
if update_result.returncode == 0:
print(f"\n✅ {skill_name} updated successfully!")
else:
print(f"\n❌ {skill_name} update failed")
else:
print("⏭️ Skipped (invalid input)")
except ValueError:
print("❌ Invalid input. Please enter a number or 'q'")
except KeyboardInterrupt:
print("\n👋 Exiting update checker.")
break
except Exception as e:
print(f"❌ Error: {e}")
def main():
parser = argparse.ArgumentParser(description="Check SkillsMP skills for available updates")
parser.add_argument("--json", action="store_true", help="Output results in JSON format")
parser.add_argument("--api-key", help="API key (overrides file)")
parser.add_argument(
"--no-interactive",
action="store_true",
help="Disable interactive mode (exit after showing summary)",
)
args = parser.parse_args()
try:
result = check_skill_updates(api_key=args.api_key)
if args.json:
# Convert to JSON-serializable format
json_result = {
"updates": result["updates"],
"up_to_date": [
{"name": s["name"], "local_date": s["local_modified_date"]}
for s in result["up_to_date"]
],
"not_found": [s["name"] for s in result["not_found"]],
"errors": result["errors"],
}
print(json.dumps(json_result, indent=2))
else:
format_update_summary(result)
# Interactive mode (only if updates found, not disabled, and in TTY)
if result["updates"] and not args.no_interactive and sys.stdin.isatty():
interactive_details_loop(result, api_key=args.api_key)
except SkillsMPError as e:
print(f"❌ Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
SkillsMP Skill Installer
Install skills from SkillsMP marketplace with one command.
"""
import argparse
import json
import sys
import search_skills
from utils import install_skill, install_skill_from_url, list_installed_skills
def install_from_search_results(skill_index: int, search_query: str, **search_kwargs):
"""
Search for a skill and install it by index.
Args:
skill_index: Index of the skill in search results (1-based)
search_query: Search query to find the skill
**search_kwargs: Additional arguments for search_skills
"""
print(f"🔍 Searching for skills: {search_query}\n")
# Search for skills
results = search_skills.search_skills(search_query, **search_kwargs)
if not results.get("success", True):
error = results.get("error", {})
print(f"❌ Search failed: {error.get('message', 'Unknown error')}")
sys.exit(1)
data = results.get("data", {})
skills = data.get("skills", [])
if not skills:
print("❌ No skills found matching your query.")
sys.exit(1)
# Display search results
print(f"Found {data.get('total', 0)} skills:\n")
for i, skill in enumerate(skills, 1):
name = skill.get("name", "Unknown")
author = skill.get("author", "Unknown")
stars = skill.get("stars", 0)
print(f"{i}. {name} by {author} (⭐ {stars})")
print()
# Validate index
if skill_index < 1 or skill_index > len(skills):
print(f"❌ Invalid skill index. Please choose between 1 and {len(skills)}")
sys.exit(1)
selected_skill = skills[skill_index - 1]
skill_name = selected_skill.get("name", "Unknown")
skill_url = selected_skill.get("download_url", "")
skill_id = selected_skill.get("id", "")
# If no direct download URL, construct from repository
if not skill_url and skill_id:
# Assuming SkillsMP provides repository_url or similar
repo_url = selected_skill.get("repository_url", "")
if repo_url:
# Try to get the latest release asset
skill_url = f"{repo_url}/releases/latest/download/skill_name.skill"
else:
print(f"❌ Skill '{skill_name}' does not provide a download URL.")
print(" Please visit SkillsMP marketplace to download manually.")
sys.exit(1)
print(f"📦 Installing: {skill_name}\n")
try:
if skill_url.startswith("http"):
installed_path = install_skill_from_url(skill_url)
else:
# Assume it's a local path
from pathlib import Path
installed_path = install_skill(Path(skill_url))
print(f"\n✅ Successfully installed: {skill_name}")
print(f" Location: {installed_path}")
print("\n💡 You can now use this skill in Claude Code!")
except Exception as e:
print(f"\n❌ Installation failed: {e}")
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description="Install skills from SkillsMP marketplace")
subparsers = parser.add_subparsers(dest="command", help="Available commands")
# Install command
install_parser = subparsers.add_parser("install", help="Install a skill")
install_parser.add_argument("query", help="Search query to find the skill (or direct URL/path)")
install_parser.add_argument(
"--index",
type=int,
default=1,
help="Index of skill to install from search results (default: 1)",
)
install_parser.add_argument(
"--page", type=int, default=1, help="Search page number (default: 1)"
)
install_parser.add_argument(
"--sort",
choices=["stars", "recent"],
default="stars",
help="Sort search results (default: stars)",
)
# List command
list_parser = subparsers.add_parser("list", help="List installed skills")
args = parser.parse_args()
if not args.command:
parser.print_help()
sys.exit(1)
try:
if args.command == "install":
# Check if query is a URL or local path
if (
args.query.startswith("http://")
or args.query.startswith("https://")
or args.query.endswith(".skill")
):
# Direct installation from URL or file
print(f"📦 Installing from: {args.query}\n")
from pathlib import Path
if args.query.startswith("http"):
installed_path = install_skill_from_url(args.query)
else:
installed_path = install_skill(Path(args.query))
print(f"\n✅ Successfully installed!")
print(f" Location: {installed_path}")
else:
# Search and install by index
install_from_search_results(
skill_index=args.index,
search_query=args.query,
page=args.page,
sort_by=args.sort,
)
elif args.command == "list":
skills = list_installed_skills()
if skills:
print("📚 Installed skills:\n")
for skill in skills:
print(f" • {skill}")
print(f"\nTotal: {len(skills)} skills")
else:
print("❌ No skills installed.")
except Exception as e:
print(f"❌ Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
SkillsMP Keyword Search Script
Search for skills using keywords on SkillsMP marketplace.
"""
import argparse
import json
import sys
from typing import Optional
from utils import APIRequestError, SkillsMPError, make_api_request
def search_skills(
query: str,
page: int = 1,
limit: int = 20,
sort_by: str = "stars",
api_key: Optional[str] = None,
) -> dict:
"""
Search skills using keyword search.
Args:
query: Search keyword
page: Page number (default: 1)
limit: Items per page (default: 20, max: 100)
sort_by: Sort by 'stars' or 'recent' (default: 'stars')
api_key: SkillsMP API key
Returns:
dict: Search results
Raises:
SkillsMPError: If the search fails
"""
params = {"q": query, "page": page, "limit": min(limit, 100), "sortBy": sort_by}
return make_api_request("/skills/search", params, api_key=api_key)
def format_results(results):
"""Format search results for display"""
if not results.get("success", True):
error = results.get("error", {})
print(f"Error: {error.get('code', 'UNKNOWN')} - {error.get('message', 'Unknown error')}")
return
data = results.get("data", {})
skills = data.get("skills", [])
# Use total from API, fallback to actual count
total = data.get("total", len(skills))
if total == 0 and len(skills) > 0:
total = len(skills)
print(f"\n=== Search Results ===")
print(f"Total: {total} skills found\n")
for i, skill in enumerate(skills, 1):
name = skill.get("name", "Unknown")
description = skill.get("description", "No description")
stars = skill.get("stars", 0)
author = skill.get("author", "Unknown")
print(f"{i}. {name}")
print(f" Author: {author} | Stars: {stars}")
print(f" Description: {description[:100]}{'...' if len(description) > 100 else ''}")
print()
def main():
parser = argparse.ArgumentParser(description="Search SkillsMP marketplace for skills")
parser.add_argument("query", help="Search keyword")
parser.add_argument("--page", type=int, default=1, help="Page number (default: 1)")
parser.add_argument(
"--limit", type=int, default=20, help="Items per page (default: 20, max: 100)"
)
parser.add_argument(
"--sort",
choices=["stars", "recent"],
default="stars",
help="Sort by: 'stars' (default) or 'recent'",
)
parser.add_argument("--json", action="store_true", help="Output raw JSON")
parser.add_argument("--api-key", help="API key (overrides file)")
args = parser.parse_args()
try:
results = search_skills(
query=args.query,
page=args.page,
limit=args.limit,
sort_by=args.sort,
api_key=args.api_key,
)
if args.json:
print(json.dumps(results, indent=2))
else:
format_results(results)
except SkillsMPError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
SkillsMP Skill Diff Viewer
Compare local skill with remote SkillsMP version.
"""
import argparse
import json
import re
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional
from utils import APIRequestError, SkillsMPError, make_api_request
def extract_frontmatter(skill_md_path: Path) -> Dict[str, str]:
"""
Extract YAML frontmatter from SKILL.md.
Args:
skill_md_path: Path to SKILL.md file
Returns:
Dict with frontmatter fields
"""
try:
with open(skill_md_path, "r", encoding="utf-8") as f:
content = f.read()
frontmatter = {}
in_frontmatter = False
for line in content.split("\n")[:30]: # Check first 30 lines
if line.strip() == "---":
if not in_frontmatter:
in_frontmatter = True
else:
break
continue
if in_frontmatter:
match = re.match(r"^(\w+):\s*(.+)$", line)
if match:
key, value = match.groups()
frontmatter[key] = value.strip()
return frontmatter
except Exception:
return {}
def get_skill_details_from_skillsmp(
skill_name: str, api_key: Optional[str] = None
) -> Optional[Dict]:
"""
Get detailed skill information from SkillsMP.
Args:
skill_name: Name of the skill
api_key: SkillsMP API key
Returns:
Skill data or None if not found
"""
try:
params = {"q": skill_name, "limit": 5, "sortBy": "stars"}
result = make_api_request("/skills/search", params, api_key=api_key)
if not result.get("success"):
return None
skills_list = result.get("data", {}).get("skills", [])
if not skills_list:
return None
# Find exact match
for skill in skills_list:
if skill.get("name", "").lower() == skill_name.lower():
return skill
# Return best match
return skills_list[0]
except (APIRequestError, SkillsMPError):
return None
def compare_versions(local: Dict[str, str], remote: Dict[str, Any]) -> Dict[str, Any]:
"""
Compare local and remote skill versions.
Args:
local: Local frontmatter data
remote: Remote API data
Returns:
Dict with comparison results
"""
differences: Dict[str, Any] = {
"name_changed": False,
"description_changed": False,
"author_changed": False,
"stars_changed": False,
"fields": [],
}
fields_list: List[Dict[str, Any]] = differences["fields"] # type: ignore[assignment]
# Compare name
local_name = local.get("name", "")
remote_name = remote.get("name", "")
if local_name and remote_name and local_name != remote_name:
differences["name_changed"] = True
fields_list.append({"field": "name", "local": local_name, "remote": remote_name})
# Compare description
local_desc = local.get("description", "")
remote_desc = remote.get("description", "")
if local_desc and remote_desc and local_desc != remote_desc:
differences["description_changed"] = True
fields_list.append(
{"field": "description", "local": local_desc[:100], "remote": remote_desc[:100]}
)
# Compare author
local_author = local.get("author", "")
remote_author = remote.get("author", "")
if local_author and remote_author and local_author != remote_author:
differences["author_changed"] = True
fields_list.append({"field": "author", "local": local_author, "remote": remote_author})
# Compare stars (always check, not in frontmatter)
remote_stars = remote.get("stars", 0)
differences["stars_changed"] = True
fields_list.append({"field": "stars", "remote": remote_stars})
return differences
def format_diff_output(
skill_name: str, local_path: Path, remote_data: Dict[str, Any], differences: Dict[str, Any]
):
"""Format and display diff comparison."""
print("\n" + "=" * 60)
print(f"📦 {skill_name} - Version Comparison")
print("=" * 60)
# Basic info
print(f"\n📍 Local Path: {local_path}")
print(f"🔗 GitHub: {remote_data.get('githubUrl', 'N/A')}")
print(f"🌐 SkillsMP: {remote_data.get('skillUrl', 'N/A')}")
print(f"⭐ Stars: {remote_data.get('stars', 0)}")
print(f"📅 Last Updated: {remote_data.get('updatedAt', 'N/A')} " f"(Unix timestamp)")
# Differences
if differences["fields"]:
print("\n📋 Changes Detected:\n")
for field_data in differences["fields"]:
field = field_data["field"]
if field == "name":
print(f" Name:")
print(f" - Local: {field_data['local']}")
print(f" + Remote: {field_data['remote']}")
elif field == "description":
print(f" Description:")
print(f" - Local: {field_data['local']}...")
print(f" + Remote: {field_data['remote']}...")
elif field == "author":
print(f" Author:")
print(f" - Local: {field_data['local']}")
print(f" + Remote: {field_data['remote']}")
elif field == "stars":
print(f" Stars: {field_data['remote']}")
else:
print("\n✅ No differences detected in frontmatter")
print("\n" + "=" * 60)
def main():
parser = argparse.ArgumentParser(description="Compare local skill with SkillsMP remote version")
parser.add_argument("skill_name", help="Name of the skill to compare")
parser.add_argument(
"--skill-dir",
type=Path,
help="Custom skills directory (default: ~/.claude/skills)",
)
parser.add_argument("--api-key", help="API key (overrides file)")
parser.add_argument("--json", action="store_true", help="Output in JSON format")
args = parser.parse_args()
try:
# Determine skill directory
skill_dir = args.skill_dir
if not skill_dir:
from utils import get_claude_skills_dir
skill_dir = get_claude_skills_dir()
# Find local skill directory
local_skill_path = None
for item in skill_dir.iterdir():
if item.is_dir():
skill_md = item / "SKILL.md"
if skill_md.exists():
# Extract name from frontmatter
frontmatter = extract_frontmatter(skill_md)
if frontmatter.get("name", "").lower() == args.skill_name.lower():
local_skill_path = item
break
if not local_skill_path:
print(f"❌ Skill '{args.skill_name}' not found locally")
sys.exit(1)
# Get remote data
remote_data = get_skill_details_from_skillsmp(args.skill_name, api_key=args.api_key)
if not remote_data:
print(f"❌ Skill '{args.skill_name}' not found on SkillsMP")
sys.exit(1)
# Extract local frontmatter
skill_md = local_skill_path / "SKILL.md"
local_frontmatter = extract_frontmatter(skill_md)
# Compare
differences = compare_versions(local_frontmatter, remote_data)
if args.json:
# Output JSON
json_output = {
"skill_name": args.skill_name,
"local_path": str(local_skill_path),
"remote_data": remote_data,
"differences": differences,
}
print(json.dumps(json_output, indent=2))
else:
# Format output
format_diff_output(args.skill_name, local_skill_path, remote_data, differences)
except SkillsMPError as e:
print(f"❌ Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
SkillsMP Skill Downloader
Download and install skill updates from GitHub or SkillsMP.
"""
import argparse
import os
import re
import shutil
import sys
import tempfile
import zipfile
from pathlib import Path
from typing import Optional
import requests
from utils import (
APIRequestError,
SkillsMPError,
get_claude_skills_dir,
install_skill,
load_api_key,
make_api_request,
)
def infer_download_url(github_url: str, skill_name: str) -> Optional[str]:
"""
Infer download URL from GitHub URL.
Attempts to construct GitHub Releases download URL.
Args:
github_url: GitHub repository URL
skill_name: Name of the skill
Returns:
Download URL or None if cannot be inferred
"""
# Extract owner/repo from GitHub URL
# Format: https://github.com/owner/repo/tree/branch/skills/skill-name
match = re.search(r"github\.com/([^/]+)/([^/]+)", github_url)
if not match:
return None
owner, repo = match.groups()
# Try GitHub Releases latest download
# Common patterns: skill-name.skill, repo-name.skill, etc.
possible_filenames = [
f"{skill_name}.skill",
f"{repo}.skill",
f"{owner}-{repo}.skill",
]
for filename in possible_filenames:
download_url = f"https://github.com/{owner}/{repo}/releases/latest/download/{filename}"
return download_url
return None
def download_skill_file(download_url: str, dest_path: Path) -> bool:
"""
Download skill file from URL.
Args:
download_url: URL to download from
dest_path: Destination file path
Returns:
True if successful, False otherwise
"""
try:
print(f"📥 Downloading from: {download_url}")
response = requests.get(download_url, stream=True, timeout=30)
if response.status_code == 404:
print("❌ Download failed: File not found (404)")
return False
response.raise_for_status()
# Download to file
dest_path.parent.mkdir(parents=True, exist_ok=True)
with open(dest_path, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
print(f"✅ Downloaded to: {dest_path}")
return True
except requests.exceptions.RequestException as e:
print(f"❌ Download failed: {e}")
return False
def backup_skill_directory(skill_dir: Path) -> Optional[Path]:
"""
Create backup of existing skill directory.
Args:
skill_dir: Path to skill directory
Returns:
Backup directory path or None if failed
"""
try:
parent = skill_dir.parent
backup_name = f"{skill_dir.name}.backup"
backup_path = parent / backup_name
# Remove existing backup if present
if backup_path.exists():
shutil.rmtree(backup_path)
# Create backup
shutil.copytree(skill_dir, backup_path)
print(f"📦 Backup created: {backup_path}")
return backup_path
except Exception as e:
print(f"⚠️ Warning: Could not create backup: {e}")
return None
def install_skill_update(
skill_name: str,
download_url: Optional[str] = None,
github_url: Optional[str] = None,
skills_dir: Optional[Path] = None,
api_key: Optional[str] = None,
backup: bool = True,
) -> bool:
"""
Download and install skill update.
Args:
skill_name: Name of the skill to update
download_url: Direct download URL (optional)
github_url: GitHub repository URL (for inferring download URL)
skills_dir: Skills directory
api_key: API key
backup: Whether to backup existing installation
Returns:
True if successful, False otherwise
"""
if skills_dir is None:
skills_dir = get_claude_skills_dir()
# Find existing skill directory
skill_dir = None
for item in skills_dir.iterdir():
if item.is_dir() and (item / "SKILL.md").exists():
# Extract name from frontmatter
import skill_diff
frontmatter = skill_diff.extract_frontmatter(item / "SKILL.md")
if frontmatter.get("name", "").lower() == skill_name.lower():
skill_dir = item
break
if not skill_dir:
print(f"❌ Skill '{skill_name}' not found locally")
return False
# Determine download URL
if not download_url and github_url:
download_url = infer_download_url(github_url, skill_name)
if not download_url:
print("❌ No download URL available")
print(" Please provide --download-url or --github-url")
return False
# Create backup
backup_path = None
if backup:
backup_path = backup_skill_directory(skill_dir)
# Download to temp directory
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir) / "downloaded.skill"
if not download_skill_file(download_url, temp_path):
return False
# Verify it's a valid zip file (skill package)
try:
with zipfile.ZipFile(temp_path, "r") as zip_ref:
namelist = zip_ref.namelist()
if not namelist:
print("❌ Invalid skill file: empty archive")
return False
except zipfile.BadZipFile:
print("❌ Invalid skill file: not a valid zip archive")
return False
# Remove existing installation
try:
shutil.rmtree(skill_dir)
except Exception as e:
print(f"⚠️ Warning: Could not remove old installation: {e}")
# Try to remove backup and restore
if backup_path and backup_path.exists():
shutil.rmtree(backup_path)
return False
# Install new version
try:
installed_path = install_skill(temp_path, skills_dir=skills_dir)
print(f"✅ Successfully updated: {skill_name}")
print(f" Location: {installed_path}")
# Remove backup on success
if backup_path and backup_path.exists():
shutil.rmtree(backup_path)
print("🗑️ Backup removed (update successful)")
return True
except Exception as e:
print(f"❌ Installation failed: {e}")
# Restore from backup
if backup_path and backup_path.exists():
print(f"🔄 Restoring from backup...")
try:
shutil.copytree(backup_path, skill_dir)
print("✅ Successfully restored from backup")
except Exception as restore_error:
print(f"❌ Could not restore backup: {restore_error}")
return False
def main():
parser = argparse.ArgumentParser(description="Download and install skill updates from SkillsMP")
parser.add_argument("skill_name", help="Name of the skill to update")
download_group = parser.add_mutually_exclusive_group()
download_group.add_argument("--download-url", help="Direct download URL")
download_group.add_argument(
"--github-url",
help="GitHub repository URL (will infer download URL)",
)
parser.add_argument(
"--skill-dir",
type=Path,
help="Custom skills directory (default: ~/.claude/skills)",
)
parser.add_argument("--api-key", help="API key (overrides file)")
parser.add_argument(
"--no-backup",
action="store_true",
help="Skip backup before installing",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Show what would be done without actually installing",
)
args = parser.parse_args()
try:
if args.dry_run:
print("🔍 Dry run mode - showing planned actions:\n")
print(f"Skill to update: {args.skill_name}")
if args.download_url:
print(f"Download URL: {args.download_url}")
elif args.github_url:
inferred = infer_download_url(args.github_url, args.skill_name)
print(f"GitHub URL: {args.github_url}")
print(f"Inferred download URL: {inferred}")
else:
print("❌ No download source specified")
sys.exit(1)
backup_msg = "Backup: " + ("No (disabled)" if args.no_backup else "Yes")
print(backup_msg)
print("\n✅ Dry run complete - no changes made")
return
# Perform actual installation
success = install_skill_update(
skill_name=args.skill_name,
download_url=args.download_url,
github_url=args.github_url,
skills_dir=args.skill_dir,
api_key=args.api_key,
backup=not args.no_backup,
)
sys.exit(0 if success else 1)
except SkillsMPError as e:
print(f"❌ Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
SkillsMP Skill Details Viewer
View detailed information about a specific skill.
"""
import argparse
import json
import sys
from typing import Optional
from utils import APIRequestError, SkillsMPError, load_api_key, make_api_request
def get_skill_details(skill_id: str, api_key: Optional[str] = None) -> dict:
"""
Get detailed information about a specific skill.
Args:
skill_id: The ID of the skill
api_key: SkillsMP API key
Returns:
dict: Skill details
Raises:
SkillsMPError: If the request fails
"""
params = {"id": skill_id}
# Note: This endpoint may need adjustment based on actual API
return make_api_request("/skills/details", params, api_key=api_key)
def format_skill_details(skill: dict):
"""Format skill details for display"""
print("\n" + "=" * 60)
print(f"📦 {skill.get('name', 'Unknown')}")
print("=" * 60)
# Basic info
print(f"\n👤 Author: {skill.get('author', 'Unknown')}")
print(f"⭐ Stars: {skill.get('stars', 0)}")
print(f"📅 Version: {skill.get('version', 'N/A')}")
# Description
description = skill.get("description", "No description")
print(f"\n📝 Description:")
print(f" {description}")
# Categories/Tags
tags = skill.get("tags", [])
if tags:
print(f"\n🏷️ Tags: {', '.join(tags)}")
# Installation command
repo_url = skill.get("repository_url", "")
if repo_url:
print(f"\n🔗 Repository: {repo_url}")
print(f"📦 Install: npx skills add {repo_url.split('github.com/')[-1]}")
# Requirements
requirements = skill.get("requirements", [])
if requirements:
print(f"\n📋 Requirements:")
for req in requirements:
print(f" - {req}")
# Examples
examples = skill.get("examples", [])
if examples:
print(f"\n💡 Usage Examples:")
for i, example in enumerate(examples, 1):
print(f" {i}. {example}")
print("\n" + "=" * 60 + "\n")
def main():
parser = argparse.ArgumentParser(description="View detailed information about a SkillsMP skill")
parser.add_argument("skill_id", help="Skill ID or name")
parser.add_argument("--json", action="store_true", help="Output raw JSON")
parser.add_argument("--api-key", help="API key (overrides file)")
args = parser.parse_args()
try:
details = get_skill_details(args.skill_id, api_key=args.api_key)
if args.json:
print(json.dumps(details, indent=2))
else:
format_skill_details(details)
except SkillsMPError as e:
print(f"❌ Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Shared utilities for SkillsMP search scripts
"""
import os
import subprocess
import sys
import zipfile
from pathlib import Path
from typing import Dict, Optional
import requests
class SkillsMPError(Exception):
"""Base exception for SkillsMP errors"""
pass
class APIKeyError(SkillsMPError):
"""Exception raised when API key is not found or invalid"""
pass
class APIRequestError(SkillsMPError):
"""Exception raised when API request fails"""
pass
# API Configuration
BASE_URL = os.getenv("SKILLSMP_API_BASE_URL", "https://skillsmp.com/api/v1")
API_KEY_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), "references", "api_key.txt")
API_KEY_REAL_FILE = os.path.join(
os.path.dirname(os.path.dirname(__file__)), "references", "api_key_real.txt"
)
def load_api_key() -> str:
"""
Load API key from multiple sources (priority order):
1. Environment variable SKILLSMP_API_KEY
2. File references/api_key_real.txt (for development, gitignored)
3. File references/api_key.txt (template file)
Returns:
str: API key
Raises:
APIKeyError: If no valid API key is found
"""
# Try environment variable first (most secure)
env_key = os.getenv("SKILLSMP_API_KEY")
if env_key:
return env_key
# Helper function to read first non-comment line from file
def read_first_valid_key(file_path: Path) -> Optional[str]:
try:
with open(file_path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
# Skip empty lines and comments
if line and not line.startswith("#"):
# Skip placeholder text
if "your_api_key_here" not in line.lower():
return line
except FileNotFoundError:
pass
return None
# Try api_key_real.txt (for development)
key = read_first_valid_key(Path(API_KEY_REAL_FILE))
if key:
return key
# Try api_key.txt (template file)
key = read_first_valid_key(Path(API_KEY_FILE))
if key:
return key
# No valid API key found
raise APIKeyError(
"No valid API key found.\n\n"
"Please configure your API key using one of these methods:\n"
"1. Set environment variable SKILLSMP_API_KEY (recommended)\n"
"2. Create file: references/api_key_real.txt\n"
"3. Edit file: references/api_key.txt\n\n"
"See README.md for detailed instructions."
)
def load_proxies() -> Optional[Dict[str, str]]:
"""
Load proxy settings from environment variables.
Returns:
Dict with 'http' and 'https' proxy URLs, or None if no proxies configured
"""
http_proxy = os.environ.get("HTTP_PROXY") or os.environ.get("http_proxy")
https_proxy = os.environ.get("HTTPS_PROXY") or os.environ.get("https_proxy")
if http_proxy or https_proxy:
proxies = {}
if http_proxy:
proxies["http"] = http_proxy
if https_proxy:
proxies["https"] = https_proxy
return proxies
return None
def make_api_request(
endpoint: str,
params: Dict,
api_key: Optional[str] = None,
timeout: int = 10,
) -> Dict:
"""
Make an API request to SkillsMP with error handling and proxy support.
Args:
endpoint: API endpoint (e.g., '/skills/search')
params: Query parameters
api_key: SkillsMP API key (if None, will load from config)
timeout: Request timeout in seconds (default: 10)
Returns:
dict: API response data
Raises:
APIKeyError: If API key is not found
APIRequestError: If the request fails
"""
if api_key is None:
api_key = load_api_key()
url = f"{BASE_URL}{endpoint}"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
proxies = load_proxies()
try:
response = requests.get(
url, headers=headers, params=params, proxies=proxies, timeout=timeout
)
response.raise_for_status()
return response.json()
except requests.exceptions.HTTPError as e:
if response.status_code == 401:
error_data = response.json()
raise APIRequestError(
f"API authentication failed: {error_data.get('error', {}).get('message', 'Invalid API key')}"
) from e
raise APIRequestError(f"HTTP error {response.status_code}: {e}") from e
except requests.exceptions.Timeout:
raise APIRequestError(f"Request timed out after {timeout} seconds") from None
except requests.exceptions.RequestException as e:
raise APIRequestError(f"Request failed: {e}") from e
def get_claude_skills_dir() -> Path:
"""
Get the Claude Code skills directory.
Returns:
Path: Path to Claude skills directory
Raises:
SkillsMPError: If Claude skills directory cannot be found
"""
# Try common Claude Code skills directories
possible_paths = [
Path.home() / ".claude" / "skills",
Path.home() / "AppData" / "Roaming" / "claude" / "skills", # Windows
Path.home() / ".config" / "claude" / "skills", # Linux/macOS
]
for path in possible_paths:
if path.exists():
return path
# If none exist, create the default one
default_path = Path.home() / ".claude" / "skills"
try:
default_path.mkdir(parents=True, exist_ok=True)
return default_path
except OSError as e:
raise SkillsMPError(f"Cannot create Claude skills directory: {e}") from e
def download_skill(skill_url: str, download_dir: Path) -> Path:
"""
Download a skill file from URL.
Args:
skill_url: URL to download the skill from
download_dir: Directory to save the downloaded file
Returns:
Path: Path to downloaded skill file
Raises:
APIRequestError: If download fails
"""
proxies = load_proxies()
try:
response = requests.get(skill_url, proxies=proxies, timeout=30, stream=True)
response.raise_for_status()
# Extract filename from URL or use default
filename = skill_url.split("/")[-1] or "downloaded_skill.skill"
skill_path = download_dir / filename
with open(skill_path, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
return skill_path
except requests.exceptions.RequestException as e:
raise APIRequestError(f"Failed to download skill: {e}") from e
def install_skill(skill_path: Path, skills_dir: Optional[Path] = None) -> Path:
"""
Install a skill from a .skill file to Claude Code skills directory.
Args:
skill_path: Path to the .skill file
skills_dir: Custom skills directory (if None, uses default Claude location)
Returns:
Path: Path to installed skill directory
Raises:
SkillsMPError: If installation fails
"""
if skills_dir is None:
skills_dir = get_claude_skills_dir()
if not skill_path.exists():
raise SkillsMPError(f"Skill file not found: {skill_path}")
# Extract the skill file
try:
with zipfile.ZipFile(skill_path, "r") as zip_ref:
# Get the root directory name in the zip
namelist = zip_ref.namelist()
if not namelist:
raise SkillsMPError("Skill file is empty")
# Extract all contents
zip_ref.extractall(skills_dir)
# Find the extracted skill directory
# Typically the first entry is the skill directory
first_item = namelist[0]
skill_dir_name = first_item.split("/")[0]
installed_path = skills_dir / skill_dir_name
print(f"Skill installed to: {installed_path}")
return installed_path
except zipfile.BadZipFile:
raise SkillsMPError(f"Invalid skill file: {skill_path}") from None
except Exception as e:
raise SkillsMPError(f"Failed to install skill: {e}") from e
def install_skill_from_url(skill_url: str, skills_dir: Optional[Path] = None) -> Path:
"""
Download and install a skill from URL.
Args:
skill_url: URL to download the skill from
skills_dir: Custom skills directory (if None, uses default Claude location)
Returns:
Path: Path to installed skill directory
Raises:
APIRequestError: If download fails
SkillsMPError: If installation fails
"""
# Download to temp directory
import tempfile
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
skill_file = download_skill(skill_url, temp_path)
return install_skill(skill_file, skills_dir)
def list_installed_skills(skills_dir: Optional[Path] = None) -> list[str]:
"""
List all installed skills in Claude Code skills directory.
Args:
skills_dir: Custom skills directory (if None, uses default Claude location)
Returns:
list[str]: List of installed skill names
"""
if skills_dir is None:
skills_dir = get_claude_skills_dir()
if not skills_dir.exists():
return []
skills = []
for item in skills_dir.iterdir():
if item.is_dir() and (item / "SKILL.md").exists():
skills.append(item.name)
return sorted(skills)
# Tests package for SkillsMP Searcher
"""
Pytest configuration and shared fixtures for SkillsMP Searcher tests
"""
import os
from unittest.mock import patch
import pytest
@pytest.fixture
def mock_api_response():
"""Mock successful API response fixture"""
return {
"success": True,
"data": {
"skills": [
{
"id": "test-skill-1",
"name": "Test Skill",
"description": "A test skill for unit testing",
"author": "Test Author",
"stars": 42,
"relevance_score": 0.95,
},
{
"id": "test-skill-2",
"name": "Another Test Skill",
"description": "Another test skill",
"author": "Another Author",
"stars": 10,
"relevance_score": 0.87,
},
],
"total": 2,
"page": 1,
"limit": 20,
},
}
@pytest.fixture
def mock_api_error_response():
"""Mock API error response fixture"""
return {
"success": False,
"error": {
"code": "INVALID_API_KEY",
"message": "The provided API key is invalid",
},
}
@pytest.fixture
def mock_env_api_key():
"""Mock environment variable API key"""
with patch.dict(os.environ, {"SKILLSMP_API_KEY": "test_key_123"}):
yield
@pytest.fixture
def temp_api_key_file(tmp_path):
"""Create a temporary API key file"""
api_key_file = tmp_path / "api_key_real.txt"
api_key_file.write_text("sk_test_key_456")
return api_key_file
"""
Unit tests for SkillsMP Searcher scripts
"""
import os
import sys
from unittest.mock import Mock, patch
import pytest
import requests
# Add scripts directory to path
sys.path.insert(
0,
os.path.join(
os.path.dirname(__file__), "..", "skills", "skillsmp-searcher", "scripts"
),
)
import ai_search
import search_skills
import utils
from utils import APIKeyError, APIRequestError, SkillsMPError
class TestAPIKeyLoading:
"""Test API key loading from various sources"""
def test_load_from_env_var(self, monkeypatch):
"""Test loading API key from environment variable"""
monkeypatch.setenv("SKILLSMP_API_KEY", "test_key_123")
key = utils.load_api_key()
assert key == "test_key_123"
def test_load_from_real_file(self, tmp_path, monkeypatch):
"""Test loading API key from api_key_real.txt"""
api_key_file = tmp_path / "api_key_real.txt"
api_key_file.write_text("sk_test_key_456")
with patch.object(utils, "API_KEY_REAL_FILE", str(api_key_file)):
monkeypatch.delenv("SKILLSMP_API_KEY", raising=False)
key = utils.load_api_key()
assert key == "sk_test_key_456"
def test_load_from_template_file(self, tmp_path, monkeypatch):
"""Test loading API key from api_key.txt template"""
api_key_file = tmp_path / "api_key.txt"
api_key_file.write_text("sk_live_real_key_789")
with patch.object(utils, "API_KEY_FILE", str(api_key_file)):
with patch.object(
utils, "API_KEY_REAL_FILE", str(tmp_path / "nonexistent.txt")
):
monkeypatch.delenv("SKILLSMP_API_KEY", raising=False)
key = utils.load_api_key()
assert key == "sk_live_real_key_789"
def test_load_template_file_skips_placeholder(self, tmp_path, monkeypatch):
"""Test that placeholder text in template file is skipped"""
api_key_file = tmp_path / "api_key.txt"
api_key_file.write_text("sk_live_your_api_key_here")
with patch.object(utils, "API_KEY_FILE", str(api_key_file)):
with patch.object(
utils, "API_KEY_REAL_FILE", str(tmp_path / "nonexistent.txt")
):
monkeypatch.delenv("SKILLSMP_API_KEY", raising=False)
with pytest.raises(APIKeyError):
utils.load_api_key()
def test_load_no_api_key_raises_exception(self, tmp_path, monkeypatch):
"""Test APIKeyError is raised when no API key is found"""
empty_dir = tmp_path / "empty"
empty_dir.mkdir()
with patch.object(
utils, "API_KEY_REAL_FILE", str(empty_dir / "nonexistent.txt")
):
with patch.object(
utils, "API_KEY_FILE", str(empty_dir / "nonexistent.txt")
):
monkeypatch.delenv("SKILLSMP_API_KEY", raising=False)
with pytest.raises(APIKeyError, match="No valid API key found"):
utils.load_api_key()
class TestProxyLoading:
"""Test proxy configuration loading"""
def test_load_http_proxy(self, monkeypatch):
"""Test loading HTTP proxy"""
monkeypatch.setenv("HTTP_PROXY", "http://proxy.example.com:8080")
proxies = utils.load_proxies()
assert proxies is not None
assert proxies["http"] == "http://proxy.example.com:8080"
def test_load_https_proxy(self, monkeypatch):
"""Test loading HTTPS proxy"""
monkeypatch.setenv("HTTPS_PROXY", "https://proxy.example.com:8443")
proxies = utils.load_proxies()
assert proxies is not None
assert proxies["https"] == "https://proxy.example.com:8443"
def test_load_both_proxies(self, monkeypatch):
"""Test loading both HTTP and HTTPS proxies"""
monkeypatch.setenv("HTTP_PROXY", "http://proxy.example.com:8080")
monkeypatch.setenv("HTTPS_PROXY", "https://proxy.example.com:8443")
proxies = utils.load_proxies()
assert proxies is not None
assert proxies["http"] == "http://proxy.example.com:8080"
assert proxies["https"] == "https://proxy.example.com:8443"
def test_no_proxy_returns_none(self, monkeypatch):
"""Test that None is returned when no proxy is configured"""
monkeypatch.delenv("HTTP_PROXY", raising=False)
monkeypatch.delenv("HTTPS_PROXY", raising=False)
monkeypatch.delenv("http_proxy", raising=False)
monkeypatch.delenv("https_proxy", raising=False)
proxies = utils.load_proxies()
assert proxies is None
class TestSearchFunctions:
"""Test search functionality"""
@patch("utils.requests.get")
def test_search_skills_success(self, mock_get, mock_api_response):
"""Test successful API call for keyword search"""
mock_response = Mock()
mock_response.json.return_value = mock_api_response
mock_response.raise_for_status = Mock()
mock_get.return_value = mock_response
result = search_skills.search_skills("test", api_key="test_key")
assert result["success"] is True
assert len(result["data"]["skills"]) == 2
mock_get.assert_called_once()
@patch("utils.requests.get")
def test_search_skills_with_timeout(self, mock_get, mock_api_response):
"""Test that timeout is passed to requests"""
mock_response = Mock()
mock_response.json.return_value = mock_api_response
mock_response.raise_for_status = Mock()
mock_get.return_value = mock_response
search_skills.search_skills("test", api_key="test_key")
# Check that timeout was passed
call_kwargs = mock_get.call_args[1]
assert "timeout" in call_kwargs
assert call_kwargs["timeout"] == 10
@patch("utils.requests.get")
def test_search_skills_with_custom_timeout(self, mock_get, mock_api_response):
"""Test custom timeout parameter"""
mock_response = Mock()
mock_response.json.return_value = mock_api_response
mock_response.raise_for_status = Mock()
mock_get.return_value = mock_response
utils.make_api_request(
"/skills/search", {"q": "test"}, api_key="test_key", timeout=5
)
call_kwargs = mock_get.call_args[1]
assert call_kwargs["timeout"] == 5
@patch("utils.requests.get")
def test_search_skills_error_401(self, mock_get):
"""Test API authentication error handling"""
mock_response = Mock()
mock_response.status_code = 401
mock_response.json.return_value = {
"success": False,
"error": {"code": "INVALID_API_KEY", "message": "Invalid key"},
}
mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError()
mock_get.return_value = mock_response
with pytest.raises(APIRequestError, match="authentication failed"):
search_skills.search_skills("test", api_key="invalid_key")
@patch("utils.requests.get")
def test_search_skills_timeout(self, mock_get):
"""Test request timeout handling"""
mock_get.side_effect = requests.exceptions.Timeout()
with pytest.raises(APIRequestError, match="timed out"):
search_skills.search_skills("test", api_key="test_key")
@patch("utils.requests.get")
def test_ai_search_success(self, mock_get, mock_api_response):
"""Test successful AI semantic search"""
mock_response = Mock()
mock_response.json.return_value = mock_api_response
mock_response.raise_for_status = Mock()
mock_get.return_value = mock_response
result = ai_search.ai_search("How to create a scraper", api_key="test_key")
assert result["success"] is True
assert len(result["data"]["skills"]) == 2
class TestResultFormatting:
"""Test result formatting functions"""
def test_format_results_success(self, mock_api_response, capsys):
"""Test successful result formatting"""
search_skills.format_results(mock_api_response)
captured = capsys.readouterr()
assert "Test Skill" in captured.out
assert "Test Author" in captured.out
assert "42" in captured.out # stars
def test_format_results_error(self, mock_api_error_response, capsys):
"""Test error result formatting"""
search_skills.format_results(mock_api_error_response)
captured = capsys.readouterr()
assert "Error" in captured.out
assert "INVALID_API_KEY" in captured.out
def test_ai_format_results_success(self, mock_api_response, capsys):
"""Test AI search result formatting"""
ai_search.format_results(mock_api_response)
captured = capsys.readouterr()
assert "AI Search Results" in captured.out
assert "Test Skill" in captured.out
assert "0.95" in captured.out # relevance score
def test_ai_format_results_empty(self, capsys):
"""Test AI search with no results"""
empty_response = {"success": True, "data": {"skills": []}}
ai_search.format_results(empty_response)
captured = capsys.readouterr()
assert "No skills found" in captured.out
class TestIntegration:
"""Integration tests with utils module"""
@patch("utils.requests.get")
def test_search_without_api_key_uses_utils_loader(
self, mock_get, mock_api_response, monkeypatch
):
"""Test that search uses utils.load_api_key when no key provided"""
# Set up API key in environment
monkeypatch.setenv("SKILLSMP_API_KEY", "env_key_123")
mock_response = Mock()
mock_response.json.return_value = mock_api_response
mock_response.raise_for_status = Mock()
mock_get.return_value = mock_response
# Call without api_key parameter
result = search_skills.search_skills("test")
# Verify the request was made
assert result["success"] is True
# Check that the Authorization header was set
call_args = mock_get.call_args
headers = call_args[1]["headers"]
assert "Bearer env_key_123" in headers["Authorization"]
class TestSkillUpdateChecker:
"""Test skill update checking functionality"""
def test_extract_skill_name_from_md(self, tmp_path):
"""Test extracting skill name from SKILL.md"""
import check_updates
# Create a test SKILL.md file
skill_md = tmp_path / "SKILL.md"
skill_md.write_text(
"---\nname: test-skill\nauthor: Test Author\n---\n\n# Test Skill\n"
)
name = check_updates.get_skill_name_from_md(skill_md)
assert name == "test-skill"
def test_extract_skill_name_no_frontmatter(self, tmp_path):
"""Test handling SKILL.md without frontmatter"""
import check_updates
skill_md = tmp_path / "SKILL.md"
skill_md.write_text("# Test Skill\n\nNo frontmatter here")
name = check_updates.get_skill_name_from_md(skill_md)
assert name is None
@patch("check_updates.make_api_request")
def test_search_skill_on_skillsmp_success(self, mock_get):
"""Test successful skill search on SkillsMP"""
import check_updates
mock_response = {
"success": True,
"data": {
"skills": [
{
"id": "test-skill-1",
"name": "test-skill",
"author": "Test Author",
"description": "A test skill",
"githubUrl": "https://github.com/test/skill",
"skillUrl": "https://skillsmp.com/skills/test-skill",
"stars": 100,
"updatedAt": 1704067200, # 2024-01-01
}
]
},
}
mock_get.return_value = mock_response
result = check_updates.search_skill_on_skillsmp("test-skill")
assert result is not None
assert result["name"] == "test-skill"
assert result["updatedAt"] == 1704067200
@patch("check_updates.make_api_request")
def test_search_skill_on_skillsmp_not_found(self, mock_get):
"""Test skill search when skill not found"""
import check_updates
mock_response = {"success": True, "data": {"skills": []}}
mock_get.return_value = mock_response
result = check_updates.search_skill_on_skillsmp("nonexistent-skill")
assert result is None
def test_format_timestamp(self):
"""Test timestamp formatting"""
import check_updates
# Unix timestamp for 2024-01-01 00:00:00 UTC
timestamp = 1704067200
formatted = check_updates.format_timestamp(timestamp)
assert formatted == "2024-01-01"
@patch("check_updates.search_skill_on_skillsmp")
def test_check_skill_updates_with_update(self, mock_search, tmp_path):
"""Test update check when update is available"""
import check_updates
import os
# Create a test skills directory with an old skill
skills_dir = tmp_path / "skills"
skills_dir.mkdir()
skill_dir = skills_dir / "test-skill"
skill_dir.mkdir()
skill_md = skill_dir / "SKILL.md"
skill_md.write_text("---\nname: test-skill\n---\n")
# Set local modification time to 2023-12-31 (older than API response)
old_time = 1703980800 # 2023-12-31 00:00:00 UTC
os.utime(skill_dir, (old_time, old_time))
# Mock API response with newer timestamp (2024-01-01)
mock_search.return_value = {
"name": "test-skill",
"updatedAt": 1704067200, # 2024-01-01 (newer)
"githubUrl": "https://github.com/test/skill",
"skillUrl": "https://skillsmp.com/skills/test-skill",
"stars": 100,
}
result = check_updates.check_skill_updates(skills_dir=skills_dir)
assert len(result["updates"]) == 1
assert result["updates"][0]["name"] == "test-skill"