
Skill Finder
- 552 installs
- 23 repo stars
- Updated August 4, 2026
- aktsmm/agent-skills
skill-finder is an Agent Skills management skill that searches, installs, and organizes reusable SKILL.md packages from GitHub or local paths for developers who need to discover and govern agent customization primitives.
About
skill-finder is a full-featured Agent Skills management skill authored by yamapan under CC BY-NC-SA 4.0. It helps developers search GitHub and local directories for reusable skills, install them into an agent environment, and manage an existing skill collection over time. Beyond discovery, skill-finder guides when a dedicated skill is the right primitive versus a prompt, instruction, hook, or standalone agent configuration. Developers reach for skill-finder when they want to find skill packages for a specific task or domain, install new capabilities quickly, or rationalize an growing library of agent extensions. The skill is user-invocable and accepts keyword or use-case arguments to narrow searches. It fits teams standardizing how agents load domain expertise from filesystem-based SKILL.md modules rather than ad-hoc copy-paste instructions.
- Search and install skills locally or from GitHub
- Star and manage your personal skill collection
- Recommendation Gate with 5 primitive types (prompt, instruction, agent, hook, skill)
- Checks whether the ask really calls for a skill before recommending one
- Japanese and English keyword support via argument-hint
Skill Finder by the numbers
- 552 all-time installs (skills.sh)
- Ranked #1,665 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/aktsmm/agent-skills --skill skill-finderAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 552 |
|---|---|
| repo stars | ★ 23 |
| Last updated | August 4, 2026 |
| Repository | aktsmm/agent-skills ↗ |
How do you find and install agent skills?
Quickly discover, evaluate, install, and manage reusable agent skills from GitHub or locally while knowing when a prompt, instruction, hook or agent is the better primi
Who is it for?
Developers building agent workflows who need to discover, install, and govern reusable SKILL.md capabilities from GitHub or local repos.
Skip if: Developers who already know the exact skill slug to invoke and only need one-off execution without collection management or primitive selection guidance.
When should I use this skill?
A developer asks to find, search, install, or manage agent skills, or needs help choosing between a skill and other customization primitives.
What you get
Installed SKILL.md skill packages, a curated local skill collection, and a documented choice among skill, prompt, instruction, hook, or agent primitives.
- Installed skill packages
- Managed skill collection
- Primitive selection recommendation
By the numbers
- Licensed under CC BY-NC-SA 4.0
- Compares five customization primitives: skill, prompt, instruction, hook, and agent
Files
Skill Finder
Full-featured Agent Skills management tool.
When to Use
- Find skill, search skill, install skill, スキル検索
- Looking for skills for a specific task or domain
- Finding and installing skills locally
- Managing favorites with star feature
- Checking whether the ask really calls for a skill before recommending one
Recommendation Gate
Before searching, check whether the user really wants a skill.
| If the ask sounds like... | Prefer... |
|---|---|
| Single slash task | Prompt |
| Always-on or file-scoped guidance | Instruction |
| Persona, tool restrictions, delegation | Agent |
| Deterministic enforcement | Hook |
| Reusable packaged workflow | Skill |
If the answer is not Skill, explain that first instead of forcing a skill recommendation.
上表は「skill を勧めるべきか」の即時ゲート。primitive 選択の詳細 SSOT は agentic-workflow-guide skill。
→ [references/customization-routing.md](references/customization-routing.md) for routing patterns
Features
| Feature | Description |
|---|---|
| Search | Local index + GitHub API + Web fallback |
| Tags | Filter by category (#azure #bicep) |
| Install | Download to local directory |
| Star | Mark and manage favorites |
| Update | Sync all sources from GitHub |
Quick Start
# Search
python scripts/search_skills.py "pdf"
python scripts/search_skills.py "#azure #development"
# Management
python scripts/search_skills.py --info skill-name
python scripts/search_skills.py --install skill-name
python scripts/search_skills.py --star skill-name
# Index
python scripts/search_skills.py --update
python scripts/search_skills.py --add-source https://github.com/owner/repoCommand Reference
| Command | Description |
|---|---|
<query> | Search skills by keyword |
#tag | Filter by category |
--info SKILL | Show skill details |
--install SKILL | Download skill locally |
--star SKILL | Add to favorites |
--list-starred | Show favorites |
--similar SKILL | Find similar skills |
--update | Update index from sources |
--add-source URL | Add new source repository |
--stats | Show index statistics |
--check | Verify dependencies |
Files
| File | Description |
|---|---|
scripts/search_skills.py | Python script |
scripts/Search-Skills.ps1 | PowerShell script |
references/skill-index.json | Skill index (220+ skills) |
references/starred-skills.json | Your starred skills |
Requirements
→ [references/setup-guide.md](references/setup-guide.md) for installation
| Tool | Required |
|---|---|
GitHub CLI (gh) | 2.0+ |
| curl | Any |
| Python or PowerShell | One of them |
Agent Instructions
→ [references/agent-instructions.md](references/agent-instructions.md) for complete guide
Core Rules
- Use "Do it? Yes/No?" style proposals
- NEVER show commands to users - execute silently
- ALWAYS include proposal block after search results
Search Response Format
{N} repos, {M} skills searched (last updated: {date})
| Skill | Description | Source | Trust |
| ----- | ----------- | ------ | ----- |
| ... | ... | ... | ... |
**Next?**
1. Install?
2. Details?
3. Update index? (last: {date})
4. Web search?Trust Levels
| Type | Badge | Description |
|---|---|---|
official | Official | Anthropic / GitHub 公式 |
awesome-list | Curated | キュレーションリスト |
community | Community | コミュニティ製(自己責任) |
Done Criteria
- [ ] Skill vs non-skill fit checked first
- [ ] Search query returns results
- [ ] Skill installed to local directory (if requested)
- [ ] Index updated successfully (if requested)
# Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)
## English
Copyright (c) 2025-2026 yamapan (aktsmm)
This work is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0
International License.
You are free to:
- **Share** — copy and redistribute the material in any medium or format
- **Adapt** — remix, transform, and build upon the material
Under the following terms:
- **Attribution** — You must give appropriate credit, provide a link to the
license, and indicate if changes were made. You may do so in any reasonable manner,
but not in any way that suggests the licensor endorses you or your use.
- **NonCommercial** — You may not use the material for commercial purposes.
*(Please contact the author if you wish to use this material for commercial purposes.)*
- **ShareAlike** — If you remix, transform, or build upon the material, you must
distribute your contributions under the same license as the original.
No additional restrictions — You may not apply legal terms or technological
measures that legally restrict others from doing anything the license permits.
**AI/ML Training Restriction** — Use of this content for AI/ML training, data
mining, or other analytical purposes is prohibited without explicit permission.
Full license text: https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode
---
## 日本語
Copyright (c) 2025-2026 yamapan (aktsmm)
この作品はクリエイティブ・コモンズ 表示-非営利-継承 4.0 国際ライセンスの下に提供されています。
あなたは以下の条件に従う限り、自由に:
- **共有** — どのようなメディアやフォーマットでも資料を複製・再配布できます
- **翻案** — 資料をリミックス、変形、および加工することができます
以下の条件に従ってください:
- **表示** — あなたは適切なクレジットを表示し、ライセンスへのリンクを提供し、
変更があったらその旨を示さなければなりません。これらは合理的であればどのような方法で
行っても構いませんが、許諾者があなたやあなたの利用行為を支持していると示唆するような
方法は除きます。
- **非営利** — あなたは営利目的でこの資料を利用してはなりません。
(※商用利用をご希望の場合は、別途ご連絡ください。)
- **継承** — もしあなたがこの資料をリミックス、変形、または加工した場合、
あなたはあなたの貢献部分を元の作品と同じライセンスの下で配布しなければなりません。
追加的な制約は課せません — あなたは、このライセンスが他の者に許諾することを法的に
制限するような法的条項や技術的手段を適用してはなりません。
**AI/MLトレーニング制限** — 本コンテンツをAI/MLモデルのトレーニング、データマイニング、
その他の解析目的での使用は明示的な許可なく禁止されています。
ライセンス全文: https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.ja
---
## Special Permission for Microsoft Employees / Microsoft 社員向け特別許諾
### English
Microsoft Corporation employees are granted permission to use, copy, modify, and
distribute this material for any purpose within the scope of their employment
duties at Microsoft, including internal business use and customer-facing
activities, without the NonCommercial restriction of this license.
This special permission applies only to work performed as part of official
Microsoft business activities.
### 日本語
Microsoft Corporation の社員は、Microsoft での業務の範疇において、本資料を社内業務
および顧客対応を含むあらゆる目的で使用、複製、改変、配布することが許諾されます。
この場合、本ライセンスの「非営利」制限は適用されません。
この特別許諾は、Microsoft の公式な業務活動の一環として行われる作業にのみ適用されます。
---
## Disclaimer / 免責事項
### English
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR
A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
### 日本語
本ソフトウェアは「現状のまま」で提供され、明示または黙示を問わず、商品性、
特定目的への適合性、および権利非侵害についての保証を含むがこれに限定されない、
いかなる種類の保証も伴いません。作者または著作権者は、契約行為、不法行為、
またはそれ以外であろうと、ソフトウェアに起因または関連し、あるいはソフトウェアの
使用またはその他の扱いによって生じる一切の請求、損害、その他の責任について
責任を負いません。
Agent Instructions
⚠️ CRITICAL: AI agents MUST follow these instructions. Prefer action proposals over verbose explanations.
Core Principle
Use "Do it? Yes/No?" style proposals.
- ❌ Bad: "If you want to add new skills, you can run the following command..."
- ✅ Good: "Update the index?"
Recommendation Workflow
When user asks for recommendations (e.g., "おすすめは?", "何かいいスキルある?"):
| Persona | Categories | Example Skills |
|---|---|---|
| オフィスワーカー | document, office | docx, xlsx, pptx, pdf |
| 開発者 | development, testing, git | test-driven-development, systematic-debugging |
| Azure エンジニア | azure, development | azure-env-builder, mcp-builder |
| デザイナー | design, creative, web | brand-guidelines, canvas-design |
| 初心者 | meta, planning | skill-creator, brainstorming |
Response Format:
1. Ask about user's role/context if unclear 2. Show top 3-5 skills with descriptions 3. Include source breakdown table 4. Propose next actions
Collection Stewardship
When the user asks what to add, promote, or clean up in a skill collection:
1. Separate candidates from proven skills. 2. Prefer skills with repeated successful use, clear triggers, and maintained resources. 3. Flag skills that are installed but rarely selected, vague in description, or duplicated by better entries. 4. Use available stats, logs, stars, or repeated user requests to distinguish popular skills from under-triggering ones. 5. Recommend sandboxing unproven skills before adding them to a default source or private marketplace.
Skill Search Workflow
0. Classify the ask first
- If the user wants always-on rules → suggest instruction
- If the user wants persona or tool boundaries → suggest agent
- If the user wants deterministic enforcement → suggest hook
- If the user wants a packaged reusable workflow → continue with skill search
→ See customization-routing.md
1. Search ALL sources in local index
- Read
references/skill-index.json - ALWAYS search ALL sources
- Check
lastUpdatedfield
2. 🌟 Recommend from results (when 3+ hits)
### 🌟 おすすめ: {skill-name}
{理由: 公式スキル、機能が豊富、用途にマッチ など}Selection criteria: 1. Official source preferred 2. Feature richness 3. Relevance to user's purpose 4. Recently updated
3. If not found → Propose web search
Not found locally. Search the web?
→ GitHub: https://github.com/search?q=path%3A**%2FSKILL.md+{query}&type=code4. 🚨 MANDATORY: Propose next actions
| Situation | Proposal |
|---|---|
| Skill found | "Install it?" |
| Good repo discovered | "Add to sources?" |
| lastUpdated > 7 days | "⚠️ Index outdated. Update?" |
| Better non-skill fit | "Use prompt/instruction/agent/hook instead?" |
🚨 Mandatory Proposal Block
ALWAYS include at the end of every search response:
**Next?**
1. 📦 Install? (which skill?)
2. 🔍 Details?
3. 🔄 Update index? (last: {date})
4. 🌐 Web search?
5. ➕ Add source?
6. 🧭 Use another primitive instead?Search Summary Format
ALWAYS start with:
🔎 {N} リポジトリ、{M} スキルから検索しました(最終更新: {date})Output Format
Trust Level Indicators
| Type | Badge | Description |
|---|---|---|
official | 🏢 Official | Anthropic / GitHub 公式 |
awesome-list | 📋 Curated | キュレーションリスト |
community | 👥 Community | コミュニティ製(自己責任) |
⚠️ Community Warning:
⚠️ コミュニティ製スキルは自己責任でご使用ください。
公式スキル(🏢)を優先することを推奨します。Skill Table
| Skill | Description | Source | Trust |
| ----- | ----------- | ----------------- | ----------- |
| docx | Word 処理 | anthropics-skills | 🏢 Official |Source Breakdown Table
Dynamically generate from skill-index.json sources array.
Agent Behavior Rules
Commands are for agents to execute, NOT to show users.
- ❌ NEVER show commands like
python scripts/search_skills.py --install - ❌ NEVER say "以下のコマンドを実行..."
- ✅ ALWAYS execute scripts silently when user approves
- ✅ ALWAYS present options as numbered menu
- ✅ ALWAYS report results after execution
Bad:
📦 インストールする?
python scripts/search_skills.py --install docx ← NG!Good:
📦 インストールする?(どのスキル?)→ User: "docx お願い" → Agent: (executes silently) → "✅ docx をインストールしました!"
Checklist Before Responding
- [ ] Checked whether the ask actually needs a skill
- [ ] Started with search summary
- [ ] Included skill table (from ALL sources)
- [ ] Included source breakdown table
- [ ] Showed
lastUpdateddate - [ ] Added numbered action menu
- [ ] Included web search option
- [ ] For collection changes, separated candidate / adopted / shared / retire decisions
Customization Routing
Use this guide before recommending a skill.
Decide Whether the User Needs a Skill
| User Intent | Best Fit |
|---|---|
| "いつもこのルールで動いて" | Instruction |
| "この slash task を作りたい" | Prompt or Skill |
| "専用 persona で動かしたい" | Agent |
| "危険コマンドを止めたい" | Hook |
| "再利用できる workflow を配布したい" | Skill |
Fast Rules
- If the user asks for a reusable package with references, scripts, or templates, recommend a skill.
- If the user only needs a focused slash command, recommend a prompt first.
- If the user mainly needs role boundaries or tool restrictions, recommend an agent.
- If the user wants behavior enforced automatically, recommend a hook.
Response Pattern
When the ask is not best served by a skill, say so before searching.
This sounds closer to an instruction than a skill because the behavior should apply automatically.
Do you want:
1. An instruction template?
2. Skills anyway?If the user still wants skills, continue the skill search normally.
Recommendation Heuristics
When multiple skills match, prefer:
1. Official or curated source 2. Clear description with trigger conditions 3. Good resource structure (scripts/, references/, assets/) 4. Recently maintained index entries
Collection Management
When deciding whether a discovered skill belongs in a shared collection, classify it first:
| State | Meaning | Action |
|---|---|---|
| Candidate | Interesting but unproven | Keep in a sandbox/source list; do not present as default |
| Adopted | Used successfully on real tasks | Star it or install locally for repeat use |
| Shared | Useful across people/repos | Add to the maintained index or private marketplace source |
| Retire | Duplicate, stale, or under-triggering | Remove from recommendations or replace with a better source |
Promotion should be based on observed usage, clear trigger fit, and maintainability rather than novelty.
Source inspiration: Anthropic, "Lessons from building Claude Code: how we use skills" - https://claude.com/blog/lessons-from-building-claude-code-how-we-use-skills
Setup Guide
Requirements and setup instructions for Skill Finder.
Requirements
Required Tools
| Tool | Version | Purpose | Install |
|---|---|---|---|
| GitHub CLI | 2.0+ | Search/install skills from GitHub | cli.github.com |
| curl | Any | Download files | Pre-installed on most systems |
Optional (choose one)
| Runtime | Version | Script |
|---|---|---|
| PowerShell | 7+ | scripts/Search-Skills.ps1 |
| Python | 3.8+ | scripts/search_skills.py |
Installation
1. Install GitHub CLI
# Windows (winget)
winget install GitHub.cli
# macOS (Homebrew)
brew install gh
# Linux (apt)
sudo apt install gh2. Authenticate
gh auth login3. Verify
gh --version
curl --versionVerify Dependencies
python scripts/search_skills.py --check
pwsh scripts/Search-Skills.ps1 -CheckExpected output:
✅ gh: installed (version 2.x.x)
✅ curl: installed
✅ All dependencies satisfiedTroubleshooting
| Issue | Solution |
|---|---|
gh: command not found | Install GitHub CLI and add to PATH |
gh auth login fails | Run gh auth login and follow prompts |
| Rate limit exceeded | Wait or use authenticated requests |
| curl SSL errors | Update curl or check network/proxy settings |
Popular Repositories
Official (type: official)
- anthropics/skills - Official Claude Skills by Anthropic
- github/awesome-copilot - Official Copilot resources by GitHub
Curated Lists (type: awesome-list)
Community (type: community)
- obra/superpowers
- Run
--list-sourcesfor full list
Categories
Dynamically extracted from skill-index.json. Run --list-categories for current list.
Common: development, testing, document, azure, web, git, agents, mcp, cloud, creative, planning
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Skill Index",
"description": "Agent Skills のインデックスファイルスキーマ",
"type": "object",
"required": ["version", "lastUpdated", "sources", "skills"],
"properties": {
"$schema": {
"type": "string",
"description": "JSON Schema への参照"
},
"version": {
"type": "string",
"description": "インデックスのバージョン(semver形式)",
"pattern": "^\\d+\\.\\d+\\.\\d+$",
"examples": ["1.0.0"]
},
"lastUpdated": {
"type": "string",
"description": "最終更新日(ISO 8601形式)",
"pattern": "^\\d{4}-\\d{2}-\\d{2}$",
"examples": ["2025-12-31"]
},
"sources": {
"type": "array",
"description": "スキルのソースリポジトリ一覧",
"items": {
"$ref": "#/definitions/source"
},
"minItems": 1
},
"skills": {
"type": "array",
"description": "スキル一覧",
"items": {
"$ref": "#/definitions/skill"
}
},
"categories": {
"type": "array",
"description": "カテゴリ定義一覧",
"items": {
"$ref": "#/definitions/category"
}
}
},
"definitions": {
"source": {
"type": "object",
"description": "スキルのソースリポジトリ",
"required": ["id", "name", "owner", "url", "type", "description"],
"properties": {
"id": {
"type": "string",
"description": "ソースの一意識別子",
"pattern": "^[a-z0-9-]+$",
"examples": ["anthropics-skills", "obra-superpowers"]
},
"name": {
"type": "string",
"description": "ソースの表示名",
"examples": ["Anthropic Skills (Official)"]
},
"owner": {
"type": "string",
"description": "GitHubオーナー名",
"examples": ["anthropics", "obra"]
},
"url": {
"type": "string",
"description": "リポジトリURL",
"format": "uri",
"pattern": "^https://github\\.com/",
"examples": ["https://github.com/anthropics/skills"]
},
"type": {
"type": "string",
"description": "ソースの種類",
"enum": ["official", "awesome-list", "community"]
},
"description": {
"type": "string",
"description": "ソースの説明"
}
},
"additionalProperties": false
},
"skill": {
"type": "object",
"description": "スキル情報",
"required": ["name", "source", "path", "categories", "description"],
"properties": {
"name": {
"type": "string",
"description": "スキル名",
"pattern": "^[a-z0-9-]+$",
"examples": ["docx", "pdf", "azure-env-builder"]
},
"source": {
"type": "string",
"description": "ソースID(sourcesのidを参照)",
"examples": ["anthropics-skills", "obra-superpowers"]
},
"path": {
"type": "string",
"description": "リポジトリ内のパス",
"examples": ["skills/docx", ".github/skills/azure-env-builder"]
},
"categories": {
"type": "array",
"description": "カテゴリタグ",
"items": {
"type": "string",
"pattern": "^[a-z0-9-]+$"
},
"minItems": 1,
"examples": [
["document", "office"],
["azure", "development"]
]
},
"description": {
"type": "string",
"description": "スキルの説明"
}
},
"additionalProperties": false
},
"category": {
"type": "object",
"description": "カテゴリ定義",
"required": ["id", "name", "description"],
"properties": {
"id": {
"type": "string",
"description": "カテゴリID",
"pattern": "^[a-z0-9-]+$"
},
"name": {
"type": "string",
"description": "カテゴリ表示名"
},
"description": {
"type": "string",
"description": "カテゴリの説明"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
{
"starred": [],
"lastUpdated": "2025-06-30"
}
Skill Finder VS Code Extension - Design Spec
Overview
Agent Skill の「skill-finder」を VS Code 拡張機能化する設計仕様書。
Why Extension?
| 項目 | Agent Skill | VS Code 拡張機能 |
|---|---|---|
| トークン消費 | 毎回 SKILL.md + スクリプト読み込み | ゼロ |
| UI | テキストベース | TreeView, QuickPick, Webview |
| 速度 | Python/PowerShell 起動オーバーヘッド | ネイティブ TypeScript |
| 配布 | 手動コピー | Marketplace からワンクリック |
| 更新 | 手動実行 | 自動更新可能 |
Core Concept
拡張機能が「スキルのインストーラー」兼「Copilot への登録係」になる。
ユーザー: "pdf スキル入れて"
↓
┌─────────────────────────────────────────┐
│ VS Code 拡張機能 │
│ 1. skill-index.json から pdf を検索 │
│ 2. GitHub から .github/skills/pdf/ DL │
│ 3. instruction file にスキル参照を追加 │
└─────────────────────────────────────────┘
↓
Copilot: agents.md 読む → pdf スキル認識 → 使える!---
Features
Phase 1: MVP
- [ ] QuickPick 検索
- [ ] skill-index.json プレインストール
- [ ] .github/skills/ へインストール
- [ ] instruction file へ自動登録
Phase 2: UX 改善
- [ ] サイドバー TreeView
- [ ] Star 管理(ワークスペース設定)
- [ ] GitHub API 連携(最新スキル取得)
Phase 3: リッチ機能
- [ ] Webview でスキル詳細・プレビュー
- [ ] 自動更新通知
- [ ] Marketplace 公開
---
Project Structure
skill-finder-vscode/
├── package.json # 拡張機能マニフェスト
├── tsconfig.json
├── src/
│ ├── extension.ts # エントリポイント
│ ├── skillIndex.ts # インデックス管理
│ ├── skillSearch.ts # 検索ロジック
│ ├── skillInstaller.ts # インストール機能
│ ├── instructionManager.ts # agents.md 更新
│ ├── treeProvider.ts # サイドバー TreeView (Phase 2)
│ └── webviewPanel.ts # スキル詳細表示 (Phase 3)
├── resources/
│ └── skill-index.json # プレインストールインデックス
└── README.md---
Settings (package.json contributes.configuration)
{
"skillFinder.instructionFile": {
"type": "string",
"default": ".github/agents.md",
"enum": [
".github/agents.md",
".github/copilot-instructions.md",
".claude/CLAUDE.md",
"custom"
],
"enumDescriptions": [
"GitHub Copilot Agents (default)",
"GitHub Copilot Instructions",
"Claude Code",
"Specify custom path"
],
"description": "File to register installed skills"
},
"skillFinder.customInstructionPath": {
"type": "string",
"default": "",
"description": "Custom path when 'custom' is selected"
},
"skillFinder.skillsDirectory": {
"type": "string",
"default": ".github/skills",
"description": "Directory to install skills"
},
"skillFinder.autoUpdateInstruction": {
"type": "boolean",
"default": true,
"description": "Automatically update instruction file on install/uninstall"
},
"skillFinder.autoCheckUpdates": {
"type": "boolean",
"default": true,
"description": "Check for index updates on startup"
},
"skillFinder.updateCheckInterval": {
"type": "number",
"default": 7,
"description": "Days between update checks"
}
}---
Commands
| Command | Description |
|---|---|
skillFinder.search | Search skills by keyword/tag |
skillFinder.browse | Browse skills by category |
skillFinder.install | Install a skill |
skillFinder.uninstall | Uninstall a skill |
skillFinder.showInstalled | Show installed skills |
skillFinder.showStarred | Show starred skills |
skillFinder.star | Star a skill |
skillFinder.unstar | Unstar a skill |
skillFinder.updateIndex | Update skill index from remote |
skillFinder.addSource | Add custom source repository |
skillFinder.refreshInstruction | Regenerate instruction file |
---
Instruction File Format
Section Marker Approach
既存のインストラクションを壊さないように、マーカーで囲む:
# My Custom Instructions
ここは手動で書いた内容...
<!-- SKILL-FINDER-START -->
## Installed Skills
The following skills are available in this workspace.
- [pdf](.github/skills/pdf/SKILL.md) - PDF processing and manipulation
- [docx](.github/skills/docx/SKILL.md) - Word document handling
<!-- SKILL-FINDER-END -->
ここも手動の内容...Enable/Disable by Comment
<!-- SKILL-FINDER-START -->
## Installed Skills
### Active
- [pdf](.github/skills/pdf/SKILL.md) - PDF processing
### Disabled
<!-- - [docx](.github/skills/docx/SKILL.md) - Word documents -->
<!-- SKILL-FINDER-END -->---
Index Management
2-Layer Structure
┌─────────────────────────────────────────────────┐
│ Extension Package (.vsix) │
│ └── resources/skill-index.json ← Pre-installed │
└─────────────────────────────────────────────────┘
↓ Copy on first launch
┌─────────────────────────────────────────────────┐
│ User Data (globalStorageUri) │
│ └── skill-index.json ← Updatable │
│ └── starred-skills.json ← User data │
│ └── custom-sources.json ← User added │
└─────────────────────────────────────────────────┘Update Flow
Extension Startup
↓
Local index exists?
├── NO → Copy bundled index
└── YES → Check for updates (if setting enabled)
↓
Older than N days?
├── YES → Show "Update available" notification
└── NO → Use local indexMerge Strategy on Update
interface SkillIndex {
version: string;
lastUpdated: string;
sources: Source[]; // Merge (keep user additions)
skills: Skill[]; // Overwrite (update to latest)
}
// Separate user data files
interface UserData {
starred: string[]; // Preserve
customSources: Source[]; // Preserve
}---
Agent Compatibility Matrix
| Agent | Instruction File | Skills Directory |
|---|---|---|
| Copilot | .github/agents.md | .github/skills/ |
| Copilot (legacy) | .github/copilot-instructions.md | .github/skills/ |
| Claude Code | .claude/CLAUDE.md | .claude/skills/ |
| Custom | User-defined | User-defined |
---
Source Files to Reference
既存の Python 実装から移植する:
skill-finder/scripts/search_skills.py- 検索ロジックskill-finder/references/skill-index.json- インデックス構造skill-finder/references/starred-skills.json- Star 保存形式
---
Development Notes
Dependencies
{
"devDependencies": {
"@types/node": "^20.x",
"@types/vscode": "^1.85.0",
"typescript": "^5.x",
"esbuild": "^0.20.x"
}
}Key VS Code APIs
vscode.window.showQuickPick()- 検索 UIvscode.window.createTreeView()- サイドバーvscode.workspace.fs- ファイル操作vscode.ExtensionContext.globalStorageUri- ユーザーデータ保存vscode.workspace.getConfiguration()- 設定取得
---
Author
yamapan (https://github.com/aktsmm)
#!/usr/bin/env python3
"""
Skill Finder - Search and manage Agent Skills.
Features:
- Local index search (fast, offline)
- GitHub Code Search API fallback
- Web search URLs as final fallback
- Add new sources (--add-source)
- Update index from sources (--update)
- Show skill details (--info)
- Install skills locally (--install)
- Star favorite skills (--star)
- Similar skill recommendations
- Statistics (--stats)
- Tag search (#azure #bicep)
Author: yamapan (https://github.com/aktsmm)
License: MIT
"""
import argparse
import json
import re
import subprocess
import sys
import urllib.parse
from datetime import date, datetime, timedelta
from pathlib import Path
from typing import Optional, List, Dict, Any
# Paths
SCRIPT_DIR = Path(__file__).parent
INDEX_PATH = SCRIPT_DIR / ".." / "references" / "skill-index.json"
STARS_PATH = SCRIPT_DIR / ".." / "references" / "starred-skills.json"
INSTALL_DIR = Path.home() / ".skills" # Default install directory
# Configuration
AUTO_UPDATE_DAYS = 7 # Auto-update if index is older than this
# =============================================================================
# Index Management
# =============================================================================
def load_index() -> Optional[Dict[str, Any]]:
"""Load skill index from JSON file."""
if not INDEX_PATH.exists():
print(f"⚠️ Index file not found: {INDEX_PATH}")
return None
with open(INDEX_PATH, "r", encoding="utf-8") as f:
return json.load(f)
def save_index(index: Dict[str, Any]) -> None:
"""Save skill index to JSON file."""
index["lastUpdated"] = date.today().isoformat()
with open(INDEX_PATH, "w", encoding="utf-8") as f:
json.dump(index, f, indent=2, ensure_ascii=False)
print(f"✅ Index saved: {INDEX_PATH}")
def is_index_outdated(index: Dict[str, Any]) -> bool:
"""Check if index is older than AUTO_UPDATE_DAYS."""
last_updated = index.get("lastUpdated", "")
if not last_updated:
return True
try:
last_date = datetime.fromisoformat(last_updated)
age = datetime.now() - last_date
return age > timedelta(days=AUTO_UPDATE_DAYS)
except ValueError:
return True
def check_and_auto_update(index: Dict[str, Any], silent: bool = False) -> Dict[str, Any]:
"""Check if index needs update and prompt user."""
if is_index_outdated(index):
last_updated = index.get("lastUpdated", "unknown")
if not silent:
print(f"\n⚠️ Index is outdated (last updated: {last_updated})")
try:
answer = input("🔄 Update now? [Y/n]: ").strip().lower()
if answer in ["", "y", "yes"]:
update_all_sources()
# Reload index
return load_index() or index
except (EOFError, KeyboardInterrupt):
print("\n Skipped")
return index
def load_stars() -> List[str]:
"""Load starred skills list."""
if not STARS_PATH.exists():
return []
with open(STARS_PATH, "r", encoding="utf-8") as f:
return json.load(f).get("starred", [])
def save_stars(starred: List[str]) -> None:
"""Save starred skills list."""
STARS_PATH.parent.mkdir(parents=True, exist_ok=True)
with open(STARS_PATH, "w", encoding="utf-8") as f:
json.dump({"starred": starred, "lastUpdated": date.today().isoformat()}, f, indent=2)
# =============================================================================
# Search Functions
# =============================================================================
def search_local(index: Dict, query: str = "", category: str = "", source: str = "",
tags: List[str] = None) -> List[Dict]:
"""Search skills in local index with optional tag support."""
results = index.get("skills", [])
# Keyword filter (supports #tag syntax)
if query:
# Extract tags from query
tag_pattern = r'#(\w+)'
extracted_tags = re.findall(tag_pattern, query)
clean_query = re.sub(tag_pattern, '', query).strip().lower()
if extracted_tags:
tags = (tags or []) + extracted_tags
if clean_query:
results = [s for s in results
if clean_query in s["name"].lower()
or clean_query in s.get("description", "").lower()]
# Tag filter (matches categories)
if tags:
results = [s for s in results
if any(tag.lower() in [c.lower() for c in s.get("categories", [])]
for tag in tags)]
# Category filter
if category:
results = [s for s in results if category in s.get("categories", [])]
# Source filter
if source:
results = [s for s in results if s.get("source") == source]
# Add source info and star status
sources = {s["id"]: s for s in index.get("sources", [])}
starred = load_stars()
for skill in results:
src = sources.get(skill.get("source"), {})
skill["sourceUrl"] = src.get("url", "")
skill["sourceName"] = src.get("name", skill.get("source", ""))
skill["starred"] = f"{skill['source']}/{skill['name']}" in starred
# Sort: starred first
results.sort(key=lambda x: (not x.get("starred", False), x["name"]))
return results
def search_github(query: str) -> List[Dict]:
"""Search skills on GitHub using gh CLI."""
print("\n🌐 Searching GitHub...")
search_query = f"{query} filename:SKILL.md" if query else "filename:SKILL.md path:.github/skills"
try:
result = subprocess.run(
["gh", "search", "code", search_query, "--json", "repository,path,url", "--limit", "15"],
capture_output=True, text=True, timeout=30
)
if result.returncode == 0:
return json.loads(result.stdout)
except FileNotFoundError:
print(" ⚠️ GitHub CLI (gh) not found. Install it for external search.")
except subprocess.TimeoutExpired:
print(" ⚠️ GitHub search timed out.")
except json.JSONDecodeError:
print(" ⚠️ Invalid response from GitHub.")
return []
def show_web_search_urls(query: str) -> None:
"""Show web search URLs for manual search."""
terms = f"claude skill {query} OR copilot skill {query} SKILL.md" if query else "claude skills SKILL.md github"
encoded = urllib.parse.quote(terms)
print("\n🔎 Web Search URLs:")
print(f" Google: https://www.google.com/search?q={encoded}")
print(f" Bing: https://www.bing.com/search?q={encoded}")
print(f" DuckDuckGo: https://duckduckgo.com/?q={encoded}")
# =============================================================================
# Source Management
# =============================================================================
def add_source(repo_url: str) -> None:
"""Add a new source repository to the index."""
index = load_index()
if not index:
return
# Parse GitHub URL
match = re.search(r"github\.com[/:]([^/]+)/([^/]+)", repo_url)
if not match:
print(f"❌ Invalid GitHub URL: {repo_url}")
return
owner, repo = match.groups()
repo = repo.rstrip(".git")
repo_full = f"{owner}/{repo}"
# Check if already exists
for src in index.get("sources", []):
if repo_full in src.get("url", ""):
print(f"⚠️ Source already exists: {src['id']}")
# Still try to update skills
update_source_skills(index, src["id"], repo_full)
return
# Create source ID
source_id = re.sub(r'[^a-z0-9-]', '-', repo.lower())
# Add source
new_source = {
"id": source_id,
"name": repo,
"url": f"https://github.com/{repo_full}",
"type": "community",
"description": f"Skills from {repo_full}"
}
index["sources"].append(new_source)
save_index(index)
print(f"\n📦 Added new source:")
print(f" ID: {source_id}")
print(f" URL: https://github.com/{repo_full}")
# Fetch skills
update_source_skills(index, source_id, repo_full)
def update_source_skills(index: Dict, source_id: str, repo_full: str) -> None:
"""Fetch and update skills from a source repository using GitHub Code Search API."""
print("\n🔍 Searching for skills...")
found_skills = []
# Method 1: Use GitHub Code Search API to find all SKILL.md files
try:
result = subprocess.run(
["gh", "api", "search/code", "-f", f"q=repo:{repo_full} filename:SKILL.md"],
capture_output=True, text=True, timeout=30
)
if result.returncode == 0:
data = json.loads(result.stdout)
items = data.get("items", [])
if items:
# Extract unique parent directories from SKILL.md paths
seen_paths = set()
for item in items:
path = item.get("path", "")
if path.endswith("SKILL.md"):
# Get parent directory (skill folder)
parent = "/".join(path.split("/")[:-1])
if parent and parent not in seen_paths:
seen_paths.add(parent)
skill_name = parent.split("/")[-1]
found_skills.append({"name": skill_name, "path": parent})
if found_skills:
print(f" 📂 Found {len(found_skills)} skills via Code Search")
for skill in found_skills:
print(f" - {skill['name']} ({skill['path']})")
except subprocess.TimeoutExpired:
print(" ⚠️ Code Search timeout, falling back to directory scan...")
except Exception as e:
print(f" ⚠️ Code Search failed ({e}), falling back to directory scan...")
# Method 2: Fallback to directory-based search if Code Search fails or returns empty
if not found_skills:
# Check common skill directories
skills_paths = ["skills", ".github/skills", ".claude/skills", "scientific-skills"]
found_in_subdir = False
for path in skills_paths:
try:
result = subprocess.run(
["gh", "api", f"repos/{repo_full}/contents/{path}"],
capture_output=True, text=True, timeout=10
)
if result.returncode == 0 and "message" not in result.stdout[:50]:
items = json.loads(result.stdout)
if isinstance(items, list):
print(f" 📂 Found {len(items)} items in {path}")
for item in items:
name = item.get("name", "")
if name and item.get("type") == "dir":
found_skills.append({"name": name, "path": f"{path}/{name}"})
print(f" - {name}")
found_in_subdir = True
break
except subprocess.TimeoutExpired:
print(f" ⚠️ Timeout checking {path}")
except FileNotFoundError:
print(" ❌ GitHub CLI (gh) not found.")
break
except Exception:
# Silently ignore other errors (JSON parse errors, network issues, etc.)
# and continue checking other paths
pass
# If no skills/ directory found, check root for SKILL.md in subdirectories
if not found_in_subdir:
try:
result = subprocess.run(
["gh", "api", f"repos/{repo_full}/contents"],
capture_output=True, text=True, timeout=10
)
if result.returncode == 0:
items = json.loads(result.stdout)
if isinstance(items, list):
dirs = [item for item in items if item.get("type") == "dir"]
skill_dirs = []
for d in dirs:
name = d.get("name", "")
if name.startswith(".") or name in ["docs", "examples", "tests", "node_modules", "dist", "build"]:
continue
check_result = subprocess.run(
["gh", "api", f"repos/{repo_full}/contents/{name}/SKILL.md"],
capture_output=True, text=True, timeout=5
)
if check_result.returncode == 0 and "message" not in check_result.stdout[:50]:
skill_dirs.append({"name": name, "path": name})
if skill_dirs:
print(f" 📂 Found {len(skill_dirs)} skills at root level")
for skill in skill_dirs:
found_skills.append(skill)
print(f" - {skill['name']}")
except subprocess.TimeoutExpired:
print(" ⚠️ Timeout checking root")
except Exception:
# Silently ignore errors when checking root directory
# (e.g., rate limits, permission issues)
pass
# Add found skills to index
if found_skills:
print("\n✨ Adding skills to index...")
added = 0
for skill in found_skills:
existing = [s for s in index["skills"]
if s["name"] == skill["name"] and s["source"] == source_id]
if not existing:
index["skills"].append({
"name": skill["name"],
"source": source_id,
"path": skill["path"],
"categories": ["community"],
"description": f"{skill['name']} skill"
})
print(f" ✅ {skill['name']}")
added += 1
else:
print(f" ⏭️ {skill['name']} (exists)")
if added > 0:
save_index(index)
else:
print(" ⚠️ No skills found")
def update_all_sources() -> None:
"""Update skills from all registered sources."""
index = load_index()
if not index:
return
print("🔄 Updating all sources...")
sources = index.get("sources", [])
for src in sources:
url = src.get("url", "")
match = re.search(r"github\.com/([^/]+/[^/]+)", url)
if match:
repo_full = match.group(1)
print(f"\n📦 {src['id']} ({repo_full})")
update_source_skills(index, src["id"], repo_full)
print("\n✅ Update complete!")
# =============================================================================
# Skill Info & Install
# =============================================================================
def show_skill_info(skill_name: str) -> None:
"""Show detailed information about a skill."""
index = load_index()
if not index:
return
# Find skill
skills = [s for s in index.get("skills", []) if s["name"].lower() == skill_name.lower()]
if not skills:
print(f"❌ Skill not found: {skill_name}")
# Suggest similar
similar = find_similar_skills(index, skill_name)
if similar:
print("\n💡 Did you mean:")
for s in similar[:5]:
print(f" - {s['name']}")
return
skill = skills[0]
sources = {s["id"]: s for s in index.get("sources", [])}
src = sources.get(skill.get("source"), {})
print(f"\n📦 {skill['name']}")
print("=" * 50)
print(f"📝 Description: {skill.get('description', 'N/A')}")
print(f"📁 Source: {src.get('name', skill.get('source', 'Unknown'))}")
print(f"🏷️ Categories: {', '.join(skill.get('categories', []))}")
print(f"📂 Path: {skill.get('path', 'N/A')}")
url = src.get("url", "")
path = skill.get("path", "")
if url and path:
full_url = f"{url}/tree/main/{path}"
print(f"🔗 URL: {full_url}")
# Try to fetch SKILL.md content
if url and path:
print("\n📄 Fetching SKILL.md...")
match = re.search(r"github\.com/([^/]+/[^/]+)", url)
if match:
repo_full = match.group(1)
try:
result = subprocess.run(
["gh", "api", f"repos/{repo_full}/contents/{path}/SKILL.md",
"-H", "Accept: application/vnd.github.raw"],
capture_output=True, text=True, timeout=10
)
if result.returncode == 0:
print("\n" + "-" * 50)
# Show first 50 lines
lines = result.stdout.split('\n')[:50]
print('\n'.join(lines))
if len(result.stdout.split('\n')) > 50:
print("\n... (truncated)")
except Exception as e:
print(f" ⚠️ Could not fetch: {e}")
def install_skill(skill_name: str, target_dir: str = None) -> None:
"""Install a skill to local directory."""
index = load_index()
if not index:
return
# Find skill
skills = [s for s in index.get("skills", []) if s["name"].lower() == skill_name.lower()]
if not skills:
print(f"❌ Skill not found: {skill_name}")
return
skill = skills[0]
sources = {s["id"]: s for s in index.get("sources", [])}
src = sources.get(skill.get("source"), {})
url = src.get("url", "")
path = skill.get("path", "")
if not url or not path:
print(f"❌ Cannot install: missing URL or path information")
return
# Determine target directory
if target_dir:
install_path = Path(target_dir) / skill["name"]
else:
install_path = INSTALL_DIR / skill["name"]
match = re.search(r"github\.com/([^/]+/[^/]+)", url)
if not match:
print(f"❌ Invalid source URL")
return
repo_full = match.group(1)
print(f"📥 Installing {skill['name']}...")
print(f" From: {url}")
print(f" To: {install_path}")
# Create directory
install_path.mkdir(parents=True, exist_ok=True)
# Download files using gh CLI
try:
# Get file list
result = subprocess.run(
["gh", "api", f"repos/{repo_full}/contents/{path}"],
capture_output=True, text=True, timeout=15
)
if result.returncode != 0:
print(f"❌ Failed to list files: {result.stderr}")
return
items = json.loads(result.stdout)
if not isinstance(items, list):
items = [items]
# Download each file
for item in items:
if item.get("type") == "file":
file_name = item.get("name", "")
download_url = item.get("download_url", "")
if file_name and download_url:
print(f" 📄 {file_name}")
# Use curl
file_path = install_path / file_name
dl_result = subprocess.run(
["curl", "-sL", "-o", str(file_path), download_url],
timeout=30
)
if dl_result.returncode != 0:
print(f" ⚠️ Failed to download")
print(f"\n✅ Installed to: {install_path}")
except Exception as e:
print(f"❌ Installation failed: {e}")
# =============================================================================
# Star Management
# =============================================================================
def star_skill(skill_name: str, unstar: bool = False) -> None:
"""Star or unstar a skill."""
index = load_index()
if not index:
return
# Find skill
skills = [s for s in index.get("skills", []) if s["name"].lower() == skill_name.lower()]
if not skills:
print(f"❌ Skill not found: {skill_name}")
return
skill = skills[0]
skill_id = f"{skill['source']}/{skill['name']}"
starred = load_stars()
if unstar:
if skill_id in starred:
starred.remove(skill_id)
save_stars(starred)
print(f"☆ Unstarred: {skill['name']}")
else:
print(f"⚠️ Skill is not starred: {skill['name']}")
else:
if skill_id not in starred:
starred.append(skill_id)
save_stars(starred)
print(f"⭐ Starred: {skill['name']}")
else:
print(f"⚠️ Already starred: {skill['name']}")
def list_starred() -> None:
"""List all starred skills."""
starred = load_stars()
if not starred:
print("☆ No starred skills yet.")
print(" Use --star <skill-name> to star a skill.")
return
print(f"\n⭐ Starred Skills ({len(starred)}):")
for skill_id in starred:
print(f" - {skill_id}")
# =============================================================================
# Similar Skills & Recommendations
# =============================================================================
def find_similar_skills(index: Dict, skill_name: str, limit: int = 5) -> List[Dict]:
"""Find skills similar to the given skill name or query."""
skills = index.get("skills", [])
# First try to find the exact skill
target_skill = None
for s in skills:
if s["name"].lower() == skill_name.lower():
target_skill = s
break
if target_skill:
# Find by matching categories
target_cats = set(target_skill.get("categories", []))
similar = []
for s in skills:
if s["name"] != target_skill["name"]:
s_cats = set(s.get("categories", []))
overlap = len(target_cats & s_cats)
if overlap > 0:
similar.append((overlap, s))
similar.sort(key=lambda x: -x[0])
return [s[1] for s in similar[:limit]]
else:
# Fuzzy match by name
query_lower = skill_name.lower()
similar = []
for s in skills:
name_lower = s["name"].lower()
# Simple similarity: count matching characters
score = sum(1 for c in query_lower if c in name_lower)
if score > 0:
similar.append((score, s))
similar.sort(key=lambda x: -x[0])
return [s[1] for s in similar[:limit]]
def show_similar(skill_name: str) -> None:
"""Show skills similar to the given skill."""
index = load_index()
if not index:
return
similar = find_similar_skills(index, skill_name)
if similar:
print(f"\n💡 Skills similar to '{skill_name}':")
for s in similar:
print(f" - {s['name']} ({', '.join(s.get('categories', [])[:3])})")
else:
print(f" No similar skills found.")
# =============================================================================
# Post-Search Suggestions
# =============================================================================
def show_post_search_suggestions(index: Dict, query: str, results: List[Dict]) -> None:
"""Show helpful suggestions after search."""
print("\n" + "━" * 50)
print("💡 Suggestions")
# 1. Related categories
if results:
all_categories = set()
for r in results[:3]:
all_categories.update(r.get("categories", []))
if all_categories:
cats_str = ", ".join(list(all_categories)[:3])
print(f" 🏷️ Related categories: {cats_str}")
print(f" → Example: python scripts/search_skills.py \"#{list(all_categories)[0]}\"")
# 2. Similar skills
similar = find_similar_skills(index, query, limit=3)
unshown = [s for s in similar if s not in results]
if unshown:
print(f"\n 🔍 You might also like:")
for s in unshown[:3]:
print(f" - {s['name']}: {s.get('description', '')[:40]}")
# 3. Starred skills count
starred = load_stars()
if starred and len(starred) > 0:
print(f"\n ⭐ Your favorites: {len(starred)} skills")
def prompt_discover_new_repos(query: str) -> None:
"""Ask user if they want to discover new repositories."""
print("\n" + "━" * 50)
try:
answer = input("🌐 Search for more repositories? [y/N]: ").strip().lower()
if answer in ["y", "yes"]:
print("\n🔍 Searching GitHub for related repositories...")
discover_new_repos(query)
except (EOFError, KeyboardInterrupt):
print("\n Skipped")
def discover_new_repos(query: str) -> None:
"""Search for new skill repositories on GitHub."""
search_terms = f"{query} SKILL.md agent skills" if query else "SKILL.md agent skills claude copilot"
try:
# Repository search
result = subprocess.run(
["gh", "search", "repos", search_terms, "--json", "nameWithOwner,description,stargazersCount", "--limit", "10"],
capture_output=True, text=True, timeout=30
)
if result.returncode == 0:
repos = json.loads(result.stdout)
if repos:
print(f"\n📦 Found {len(repos)} repositories:")
for i, repo in enumerate(repos, 1):
name = repo.get("nameWithOwner", "")
desc = repo.get("description", "")[:50] or "No description"
stars = repo.get("stargazersCount", 0)
print(f"\n [{i}] {name} ⭐{stars}")
print(f" {desc}")
# Ask to add to index
print("\n" + "-" * 40)
try:
choice = input("📥 Enter repository number to add (blank to skip): ").strip()
if choice.isdigit():
idx = int(choice) - 1
if 0 <= idx < len(repos):
repo_name = repos[idx].get("nameWithOwner", "")
add_source(f"https://github.com/{repo_name}")
except (EOFError, KeyboardInterrupt):
pass # User cancelled input, skip adding repository
else:
print(" No matching repositories found")
else:
print(f" ⚠️ Search failed: {result.stderr}")
except FileNotFoundError:
print(" ⚠️ GitHub CLI (gh) not found")
except subprocess.TimeoutExpired:
print(" ⚠️ Search timed out")
except Exception as e:
print(f" ⚠️ Error: {e}")
# =============================================================================
# Statistics
# =============================================================================
def show_statistics() -> None:
"""Show skill index statistics."""
index = load_index()
if not index:
return
skills = index.get("skills", [])
sources = index.get("sources", [])
categories = index.get("categories", [])
starred = load_stars()
print("\n📊 Skill Index Statistics")
print("=" * 50)
print(f"📅 Last Updated: {index.get('lastUpdated', 'Unknown')}")
print(f"📦 Total Skills: {len(skills)}")
print(f"📁 Sources: {len(sources)}")
print(f"🏷️ Categories: {len(categories)}")
print(f"⭐ Starred: {len(starred)}")
# Skills per source
print("\n📦 Skills by Source:")
source_counts = {}
for s in skills:
src = s.get("source", "unknown")
source_counts[src] = source_counts.get(src, 0) + 1
for src, count in sorted(source_counts.items(), key=lambda x: -x[1]):
print(f" {src}: {count}")
# Skills per category
print("\n🏷️ Skills by Category:")
cat_counts = {}
for s in skills:
for cat in s.get("categories", []):
cat_counts[cat] = cat_counts.get(cat, 0) + 1
for cat, count in sorted(cat_counts.items(), key=lambda x: -x[1])[:10]:
print(f" {cat}: {count}")
# =============================================================================
# Dependency Check
# =============================================================================
def check_dependencies() -> None:
"""Check if required tools are installed."""
print("\n🔧 Checking Dependencies...")
tools = [
("gh", "GitHub CLI - Required for external search and install"),
("curl", "cURL - Required for downloading files"),
("python3", "Python 3 - You're running this!"),
]
for tool, desc in tools:
try:
result = subprocess.run([tool, "--version"], capture_output=True, timeout=5)
if result.returncode == 0:
version = result.stdout.decode().split('\n')[0][:50]
print(f" ✅ {tool}: {version}")
else:
print(f" ❌ {tool}: Not working properly")
except FileNotFoundError:
print(f" ❌ {tool}: Not found - {desc}")
except Exception as e:
print(f" ⚠️ {tool}: Error - {e}")
# =============================================================================
# Output Functions
# =============================================================================
def print_results(results: List[Dict], title: str) -> None:
"""Print search results."""
print(f"\n📋 {title}:")
for i, skill in enumerate(results, 1):
star = "⭐" if skill.get("starred") else ""
print(f"\n[{i}] {skill['name']} {star}")
print(f" 📝 {skill.get('description', '')}")
print(f" 📦 {skill.get('sourceName', skill.get('source', ''))}")
print(f" 🏷️ {', '.join(skill.get('categories', []))}")
url = skill.get('sourceUrl', '')
path = skill.get('path', '')
if url and path:
print(f" 🔗 {url}/{path}")
def list_categories(index: Dict) -> None:
"""List available categories."""
print("\n📁 Available Categories:")
for cat in index.get("categories", []):
print(f" {cat['id']} - {cat.get('description', cat.get('name', ''))}")
def list_sources(index: Dict) -> None:
"""List available sources."""
print("\n📦 Available Sources:")
for src in index.get("sources", []):
print(f" {src['id']} [{src.get('type', 'community')}]")
print(f" {src.get('name', '')} - {src.get('url', '')}")
# =============================================================================
# Main
# =============================================================================
def main():
parser = argparse.ArgumentParser(
description="Skill Finder - Search and manage Agent Skills",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s "pdf" Search for skills
%(prog)s "#azure #bicep" Search by tags
%(prog)s --category development Filter by category
%(prog)s --info skill-name Show skill details
%(prog)s --install skill-name Install skill locally
%(prog)s --star skill-name Star a skill
%(prog)s --similar skill-name Find similar skills
%(prog)s --update Update all sources
%(prog)s --stats Show statistics
"""
)
# Search arguments
parser.add_argument("query", nargs="?", default="", help="Search keyword (supports #tags)")
parser.add_argument("--category", "-c", help="Filter by category")
parser.add_argument("--source", "-s", help="Filter by source")
parser.add_argument("--external", "-e", action="store_true", help="Also search GitHub")
parser.add_argument("--web", "-w", action="store_true", help="Show web search URLs")
# List arguments
parser.add_argument("--list-categories", action="store_true", help="List categories")
parser.add_argument("--list-sources", action="store_true", help="List sources")
parser.add_argument("--list-starred", action="store_true", help="List starred skills")
# Management arguments
parser.add_argument("--add-source", metavar="URL", help="Add a new source repository")
parser.add_argument("--update", action="store_true", help="Update all sources")
# Skill actions
parser.add_argument("--info", metavar="SKILL", help="Show skill details")
parser.add_argument("--install", metavar="SKILL", help="Install skill locally")
parser.add_argument("--install-dir", metavar="DIR", help="Installation directory")
parser.add_argument("--star", metavar="SKILL", help="Star a skill")
parser.add_argument("--unstar", metavar="SKILL", help="Unstar a skill")
parser.add_argument("--similar", metavar="SKILL", help="Find similar skills")
# Other
parser.add_argument("--stats", action="store_true", help="Show statistics")
parser.add_argument("--check", action="store_true", help="Check dependencies")
parser.add_argument("--no-interactive", action="store_true",
help="Disable interactive prompts (for CI/automation)")
args = parser.parse_args()
# Action handlers
if args.check:
check_dependencies()
return
if args.add_source:
add_source(args.add_source)
return
if args.update:
update_all_sources()
return
if args.info:
show_skill_info(args.info)
return
if args.install:
install_skill(args.install, args.install_dir)
return
if args.star:
star_skill(args.star)
return
if args.unstar:
star_skill(args.unstar, unstar=True)
return
if args.list_starred:
list_starred()
return
if args.similar:
show_similar(args.similar)
return
if args.stats:
show_statistics()
return
# Load index
index = load_index()
if not index:
sys.exit(1)
# 自動更新チェック(1週間以上経過していたら)
if not args.no_interactive:
index = check_and_auto_update(index)
# List mode
if args.list_categories:
list_categories(index)
return
if args.list_sources:
list_sources(index)
return
# Search mode
print("\n🔍 Searching skills...")
# Local search
local_results = search_local(index, args.query, args.category, args.source)
if local_results:
print_results(local_results, f"Found {len(local_results)} skills in local index")
else:
print(" No matches in local index")
# External search
external_found = False
if not local_results or args.external:
github_results = search_github(args.query)
if github_results:
external_found = True
print(f"\n🌐 Found {len(github_results)} on GitHub:")
for i, item in enumerate(github_results, 1):
repo = item.get("repository", {}).get("nameWithOwner", "")
print(f"\n[{i}] {repo}")
print(f" 📄 {item.get('path', '')}")
print(f" 🔗 https://github.com/{repo}")
print("\n💡 Tip: Use --add-source to add good repos to your index")
else:
print("\n No matches on GitHub")
# Web search fallback
if not local_results and not external_found and args.query:
print("\n" + "━" * 50)
print("📭 No matches found. Try web search:")
show_web_search_urls(args.query)
if args.web:
show_web_search_urls(args.query)
# Show similar if few results
if args.query and len(local_results) < 3:
similar = find_similar_skills(index, args.query, limit=3)
if similar:
print(f"\n💡 You might also like:")
for s in similar:
if s not in local_results:
print(f" - {s['name']}")
# 検索後のサジェスト表示
if args.query:
show_post_search_suggestions(index, args.query, local_results)
# 他のリポジトリを探すか聞く(ローカル結果が少ない場合、インタラクティブモード時のみ)
if args.query and len(local_results) < 5 and not args.external and not args.no_interactive:
prompt_discover_new_repos(args.query)
if __name__ == "__main__":
main()
<#
.SYNOPSIS
Search and manage Agent Skills.
.DESCRIPTION
Full-featured skill management:
- Search local index (fast, offline)
- GitHub Code Search API fallback
- Web search URLs as final fallback
- Add/update sources and skills
- Star favorite skills
- Install skills locally
- Show detailed skill info
- Tag-based search (#azure #bicep)
Author: yamapan (https://github.com/aktsmm)
License: MIT
.PARAMETER Query
Search keyword (supports #tags like "#azure #bicep")
.PARAMETER Category
Filter by category (e.g., development, testing, document)
.PARAMETER Source
Filter by source (e.g., anthropics-skills, obra-superpowers)
.PARAMETER SearchExternal
Force GitHub search even if found locally
.PARAMETER SearchWeb
Open web search in browser
.PARAMETER AddSource
Add a new source repository to the index
.PARAMETER RepoUrl
Repository URL to add (use with -AddSource)
.PARAMETER Update
Update skills from all sources
.PARAMETER Info
Show detailed skill info with SKILL.md content
.PARAMETER Install
Install skill to local directory
.PARAMETER InstallDir
Target directory for install (default: ~/.skills)
.PARAMETER Star
Star a skill
.PARAMETER Unstar
Unstar a skill
.PARAMETER ListStarred
List starred skills
.PARAMETER Similar
Find similar skills
.PARAMETER Stats
Show index statistics
.PARAMETER Check
Check tool dependencies
.PARAMETER ListCategories
List available categories
.PARAMETER ListSources
List available sources
.EXAMPLE
.\Search-Skills.ps1 -Query "pdf"
.\Search-Skills.ps1 -Query "#azure #bicep"
.\Search-Skills.ps1 -Category "development"
.\Search-Skills.ps1 -Info "skill-name"
.\Search-Skills.ps1 -Install "skill-name"
.\Search-Skills.ps1 -Star "skill-name"
.\Search-Skills.ps1 -Similar "skill-name"
.\Search-Skills.ps1 -Stats
.\Search-Skills.ps1 -Update
#>
[CmdletBinding(DefaultParameterSetName = 'Search')]
param(
[Parameter(ParameterSetName = 'Search', Position = 0)]
[string]$Query = "",
[Parameter(ParameterSetName = 'Search')]
[string]$Category = "",
[Parameter(ParameterSetName = 'Search')]
[string]$Source = "",
[Parameter(ParameterSetName = 'Search')]
[switch]$SearchExternal,
[Parameter(ParameterSetName = 'Search')]
[switch]$SearchWeb,
[Parameter(ParameterSetName = 'Update')]
[switch]$Update,
[Parameter(ParameterSetName = 'AddSource')]
[switch]$AddSource,
[Parameter(ParameterSetName = 'AddSource')]
[string]$RepoUrl = "",
[Parameter(ParameterSetName = 'Info')]
[string]$Info = "",
[Parameter(ParameterSetName = 'Install')]
[string]$Install = "",
[Parameter(ParameterSetName = 'Install')]
[string]$InstallDir = "",
[Parameter(ParameterSetName = 'Star')]
[string]$Star = "",
[Parameter(ParameterSetName = 'Unstar')]
[string]$Unstar = "",
[Parameter(ParameterSetName = 'ListStarred')]
[switch]$ListStarred,
[Parameter(ParameterSetName = 'Similar')]
[string]$Similar = "",
[Parameter(ParameterSetName = 'Stats')]
[switch]$Stats,
[Parameter(ParameterSetName = 'Check')]
[switch]$Check,
[Parameter(ParameterSetName = 'ListCategories')]
[switch]$ListCategories,
[Parameter(ParameterSetName = 'ListSources')]
[switch]$ListSources,
[Parameter()]
[switch]$NoInteractive
)
# Configuration
$AutoUpdateDays = 7 # Auto-update if index is older than this
# Get script directory
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$indexPath = Join-Path $scriptDir "..\references\skill-index.json"
$starsPath = Join-Path $scriptDir "..\references\starred-skills.json"
$defaultInstallDir = Join-Path $env:USERPROFILE ".skills"
# Load index
function Get-SkillIndex {
if (Test-Path $indexPath) {
return Get-Content $indexPath -Raw | ConvertFrom-Json
}
Write-Warning "Index file not found: $indexPath"
return $null
}
# Save index
function Save-SkillIndex {
param($Index)
$Index.lastUpdated = (Get-Date).ToString("yyyy-MM-dd")
$json = $Index | ConvertTo-Json -Depth 10
Set-Content -Path $indexPath -Value $json -Encoding UTF8
Write-Host "✅ Index saved: $indexPath" -ForegroundColor Green
}
# Load stars
function Get-StarredSkills {
if (Test-Path $starsPath) {
$data = Get-Content $starsPath -Raw | ConvertFrom-Json
return @($data.starred)
}
return @()
}
# Save stars
function Save-StarredSkills {
param([string[]]$Starred)
$data = @{
starred = $Starred
lastUpdated = (Get-Date).ToString("yyyy-MM-dd")
}
$json = $data | ConvertTo-Json -Depth 5
Set-Content -Path $starsPath -Value $json -Encoding UTF8
}
# ============================================================================
# Auto-Update Check
# ============================================================================
function Test-IndexOutdated {
param($Index)
$lastUpdated = $Index.lastUpdated
if (-not $lastUpdated) { return $true }
try {
$lastDate = [datetime]::Parse($lastUpdated)
$age = (Get-Date) - $lastDate
return $age.TotalDays -gt $AutoUpdateDays
}
catch {
return $true
}
}
function Invoke-AutoUpdateCheck {
param($Index)
if (Test-IndexOutdated -Index $Index) {
$lastUpdated = if ($Index.lastUpdated) { $Index.lastUpdated } else { "unknown" }
Write-Host "`n⚠️ Index is outdated (last updated: $lastUpdated)" -ForegroundColor Yellow
try {
$answer = Read-Host "🔄 Update now? [Y/n]"
if ($answer -eq "" -or $answer -match "^[Yy]") {
Update-AllSources
return Get-SkillIndex
}
}
catch {
Write-Host " Skipped" -ForegroundColor Gray
}
}
return $Index
}
# ============================================================================
# Post-Search Suggestions
# ============================================================================
function Show-PostSearchSuggestions {
param($Index, [string]$Query, $Results)
Write-Host "`n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor DarkGray
Write-Host "💡 Suggestions" -ForegroundColor Cyan
# 1. Related categories
if ($Results -and $Results.Count -gt 0) {
$allCategories = @()
$Results | Select-Object -First 3 | ForEach-Object {
$allCategories += $_.categories
}
$allCategories = $allCategories | Select-Object -Unique | Select-Object -First 3
if ($allCategories.Count -gt 0) {
$catsStr = $allCategories -join ", "
Write-Host " 🏷️ Related categories: $catsStr" -ForegroundColor Gray
Write-Host " → Example: .\Search-Skills.ps1 -Query `"#$($allCategories[0])`"" -ForegroundColor DarkGray
}
}
# 2. Similar skills
if ($Query -and $Index) {
$similar = $Index.skills | Where-Object {
$_.name -like "*$Query*" -or $_.description -like "*$Query*"
} | Where-Object { $_ -notin $Results } | Select-Object -First 3
if ($similar) {
Write-Host "`n 🔍 You might also like:" -ForegroundColor Gray
foreach ($s in $similar) {
$desc = if ($s.description.Length -gt 40) { $s.description.Substring(0, 40) } else { $s.description }
Write-Host " - $($s.name): $desc" -ForegroundColor DarkGray
}
}
}
# 3. Starred skills count
$starred = Get-StarredSkills
if ($starred.Count -gt 0) {
Write-Host "`n ⭐ Your favorites: $($starred.Count) skills" -ForegroundColor Yellow
}
}
function Invoke-DiscoverNewRepos {
param([string]$Query)
Write-Host "`n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor DarkGray
try {
$answer = Read-Host "🌐 Search for more repositories? [y/N]"
if ($answer -match "^[Yy]") {
Write-Host "`n🔍 Searching GitHub for related repositories..." -ForegroundColor Cyan
Find-NewRepos -Query $Query
}
}
catch {
Write-Host " Skipped" -ForegroundColor Gray
}
}
function Find-NewRepos {
param([string]$Query)
$searchTerms = if ($Query) { "$Query SKILL.md agent skills" } else { "SKILL.md agent skills claude copilot" }
try {
$result = gh search repos $searchTerms --json nameWithOwner,description,stargazersCount --limit 10 2>&1
if ($LASTEXITCODE -eq 0 -and $result) {
$repos = $result | ConvertFrom-Json
if ($repos -and $repos.Count -gt 0) {
Write-Host "`n📦 Found $($repos.Count) repositories:" -ForegroundColor Cyan
$i = 1
foreach ($repo in $repos) {
$desc = if ($repo.description.Length -gt 50) { $repo.description.Substring(0, 50) } else { $repo.description }
if (-not $desc) { $desc = "No description" }
Write-Host "`n [$i] $($repo.nameWithOwner) ⭐$($repo.stargazersCount)" -ForegroundColor White
Write-Host " $desc" -ForegroundColor Gray
$i++
}
# Ask to add to index
Write-Host "`n────────────────────────────────────────" -ForegroundColor DarkGray
try {
$choice = Read-Host "📥 Enter repository number to add (blank to skip)"
if ($choice -match "^\d+$") {
$idx = [int]$choice - 1
if ($idx -ge 0 -and $idx -lt $repos.Count) {
$repoName = $repos[$idx].nameWithOwner
Write-Host "`n📦 Adding $repoName..." -ForegroundColor Cyan
& $PSCommandPath -AddSource -RepoUrl "https://github.com/$repoName"
}
}
}
catch { }
}
else {
Write-Host " No matching repositories found" -ForegroundColor Yellow
}
}
else {
Write-Host " ⚠️ Search failed" -ForegroundColor Yellow
}
}
catch {
Write-Host " ⚠️ GitHub CLI (gh) not found" -ForegroundColor Yellow
}
}
# ============================================================================
# Dependency Check
# ============================================================================
if ($Check) {
Write-Host "`n🔧 Checking Dependencies..." -ForegroundColor Cyan
$tools = @(
@{ name = "gh"; desc = "GitHub CLI - Required for external search and install" },
@{ name = "curl"; desc = "cURL - Required for downloading files" },
@{ name = "pwsh"; desc = "PowerShell - You're running this!" }
)
foreach ($tool in $tools) {
try {
$result = & $tool.name --version 2>&1
if ($LASTEXITCODE -eq 0) {
$version = ($result | Select-Object -First 1).ToString().Substring(0, [Math]::Min(50, $result.Length))
Write-Host " ✅ $($tool.name): $version" -ForegroundColor Green
} else {
Write-Host " ❌ $($tool.name): Not working properly" -ForegroundColor Red
}
}
catch {
Write-Host " ❌ $($tool.name): Not found - $($tool.desc)" -ForegroundColor Red
}
}
exit 0
}
# ============================================================================
# Statistics
# ============================================================================
if ($Stats) {
$index = Get-SkillIndex
if (-not $index) { exit 1 }
$starred = Get-StarredSkills
Write-Host "`n📊 Skill Index Statistics" -ForegroundColor Cyan
Write-Host "=" * 50
Write-Host "📅 Last Updated: $($index.lastUpdated)" -ForegroundColor Gray
Write-Host "📦 Total Skills: $($index.skills.Count)" -ForegroundColor White
Write-Host "📁 Sources: $($index.sources.Count)" -ForegroundColor White
Write-Host "🏷️ Categories: $($index.categories.Count)" -ForegroundColor White
Write-Host "⭐ Starred: $($starred.Count)" -ForegroundColor Yellow
# Skills per source
Write-Host "`n📦 Skills by Source:" -ForegroundColor Cyan
$index.skills | Group-Object -Property source | Sort-Object -Property Count -Descending | ForEach-Object {
Write-Host " $($_.Name): $($_.Count)" -ForegroundColor Gray
}
# Skills per category
Write-Host "`n🏷️ Skills by Category:" -ForegroundColor Cyan
$catCounts = @{}
$index.skills | ForEach-Object {
foreach ($cat in $_.categories) {
$catCounts[$cat] = ($catCounts[$cat] ?? 0) + 1
}
}
$catCounts.GetEnumerator() | Sort-Object -Property Value -Descending | Select-Object -First 10 | ForEach-Object {
Write-Host " $($_.Key): $($_.Value)" -ForegroundColor Gray
}
exit 0
}
# ============================================================================
# Star Management
# ============================================================================
if ($Star) {
$index = Get-SkillIndex
if (-not $index) { exit 1 }
$skill = $index.skills | Where-Object { $_.name -eq $Star } | Select-Object -First 1
if (-not $skill) {
Write-Host "❌ Skill not found: $Star" -ForegroundColor Red
exit 1
}
$skillId = "$($skill.source)/$($skill.name)"
$starred = Get-StarredSkills
if ($starred -contains $skillId) {
Write-Host "⚠️ Already starred: $($skill.name)" -ForegroundColor Yellow
} else {
$starred += $skillId
Save-StarredSkills -Starred $starred
Write-Host "⭐ Starred: $($skill.name)" -ForegroundColor Green
}
exit 0
}
if ($Unstar) {
$index = Get-SkillIndex
if (-not $index) { exit 1 }
$skill = $index.skills | Where-Object { $_.name -eq $Unstar } | Select-Object -First 1
if (-not $skill) {
Write-Host "❌ Skill not found: $Unstar" -ForegroundColor Red
exit 1
}
$skillId = "$($skill.source)/$($skill.name)"
$starred = Get-StarredSkills
if ($starred -contains $skillId) {
$starred = $starred | Where-Object { $_ -ne $skillId }
Save-StarredSkills -Starred $starred
Write-Host "☆ Unstarred: $($skill.name)" -ForegroundColor Yellow
} else {
Write-Host "⚠️ Skill is not starred: $($skill.name)" -ForegroundColor Yellow
}
exit 0
}
if ($ListStarred) {
$starred = Get-StarredSkills
if ($starred.Count -eq 0) {
Write-Host "☆ No starred skills yet." -ForegroundColor Yellow
Write-Host " Use -Star <skill-name> to star a skill." -ForegroundColor Gray
} else {
Write-Host "`n⭐ Starred Skills ($($starred.Count)):" -ForegroundColor Cyan
foreach ($s in $starred) {
Write-Host " - $s" -ForegroundColor White
}
}
exit 0
}
# ============================================================================
# Skill Info
# ============================================================================
if ($Info) {
$index = Get-SkillIndex
if (-not $index) { exit 1 }
$skill = $index.skills | Where-Object { $_.name -eq $Info } | Select-Object -First 1
if (-not $skill) {
Write-Host "❌ Skill not found: $Info" -ForegroundColor Red
# Suggest similar
$similar = $index.skills | Where-Object { $_.name -like "*$Info*" } | Select-Object -First 5
if ($similar) {
Write-Host "`n💡 Did you mean:" -ForegroundColor Cyan
foreach ($s in $similar) {
Write-Host " - $($s.name)" -ForegroundColor Gray
}
}
exit 1
}
$srcInfo = $index.sources | Where-Object { $_.id -eq $skill.source } | Select-Object -First 1
Write-Host "`n📦 $($skill.name)" -ForegroundColor Cyan
Write-Host "=" * 50
Write-Host "📝 Description: $($skill.description)" -ForegroundColor White
Write-Host "📁 Source: $($srcInfo.name)" -ForegroundColor Gray
Write-Host "🏷️ Categories: $($skill.categories -join ', ')" -ForegroundColor Gray
Write-Host "📂 Path: $($skill.path)" -ForegroundColor Gray
if ($srcInfo.url -and $skill.path) {
Write-Host "🔗 URL: $($srcInfo.url)/tree/main/$($skill.path)" -ForegroundColor Blue
}
# Fetch SKILL.md
if ($srcInfo.url -match "github\.com/([^/]+/[^/]+)") {
$repoFull = $Matches[1]
Write-Host "`n📄 Fetching SKILL.md..." -ForegroundColor Cyan
try {
$content = gh api "repos/$repoFull/contents/$($skill.path)/SKILL.md" -H "Accept: application/vnd.github.raw" 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host "-" * 50
$lines = $content -split "`n" | Select-Object -First 50
$lines | ForEach-Object { Write-Host $_ }
if (($content -split "`n").Count -gt 50) {
Write-Host "`n... (truncated)" -ForegroundColor DarkGray
}
}
}
catch {
Write-Host " ⚠️ Could not fetch SKILL.md" -ForegroundColor Yellow
}
}
exit 0
}
# ============================================================================
# Install Skill
# ============================================================================
if ($Install) {
$index = Get-SkillIndex
if (-not $index) { exit 1 }
$skill = $index.skills | Where-Object { $_.name -eq $Install } | Select-Object -First 1
if (-not $skill) {
Write-Host "❌ Skill not found: $Install" -ForegroundColor Red
exit 1
}
$srcInfo = $index.sources | Where-Object { $_.id -eq $skill.source } | Select-Object -First 1
if (-not $srcInfo.url -or -not $skill.path) {
Write-Host "❌ Cannot install: missing URL or path information" -ForegroundColor Red
exit 1
}
$targetDir = if ($InstallDir) { $InstallDir } else { $defaultInstallDir }
$installPath = Join-Path $targetDir $skill.name
if ($srcInfo.url -notmatch "github\.com/([^/]+/[^/]+)") {
Write-Host "❌ Invalid source URL" -ForegroundColor Red
exit 1
}
$repoFull = $Matches[1]
Write-Host "📥 Installing $($skill.name)..." -ForegroundColor Cyan
Write-Host " From: $($srcInfo.url)" -ForegroundColor Gray
Write-Host " To: $installPath" -ForegroundColor Gray
# Create directory
if (-not (Test-Path $installPath)) {
New-Item -ItemType Directory -Path $installPath -Force | Out-Null
}
try {
# Get file list
$items = gh api "repos/$repoFull/contents/$($skill.path)" 2>&1 | ConvertFrom-Json
foreach ($item in $items) {
if ($item.type -eq "file" -and $item.name -and $item.download_url) {
Write-Host " 📄 $($item.name)" -ForegroundColor Gray
$filePath = Join-Path $installPath $item.name
curl -sL -o $filePath $item.download_url
}
}
Write-Host "`n✅ Installed to: $installPath" -ForegroundColor Green
}
catch {
Write-Host "❌ Installation failed: $($_.Exception.Message)" -ForegroundColor Red
}
exit 0
}
# ============================================================================
# Similar Skills
# ============================================================================
if ($Similar) {
$index = Get-SkillIndex
if (-not $index) { exit 1 }
$skill = $index.skills | Where-Object { $_.name -eq $Similar } | Select-Object -First 1
if ($skill) {
# Find by matching categories
$targetCats = $skill.categories
$similarSkills = $index.skills | Where-Object {
$_.name -ne $skill.name -and ($_.categories | Where-Object { $targetCats -contains $_ })
} | Select-Object -First 5
if ($similarSkills) {
Write-Host "`n💡 Skills similar to '$($skill.name)':" -ForegroundColor Cyan
foreach ($s in $similarSkills) {
$cats = ($s.categories | Select-Object -First 3) -join ", "
Write-Host " - $($s.name) ($cats)" -ForegroundColor Gray
}
} else {
Write-Host " No similar skills found." -ForegroundColor Yellow
}
} else {
# Fuzzy match
$fuzzy = $index.skills | Where-Object { $_.name -like "*$Similar*" } | Select-Object -First 5
if ($fuzzy) {
Write-Host "`n💡 Skills matching '$Similar':" -ForegroundColor Cyan
foreach ($s in $fuzzy) {
Write-Host " - $($s.name)" -ForegroundColor Gray
}
} else {
Write-Host " No matching skills found." -ForegroundColor Yellow
}
}
exit 0
}
# ============================================================================
# Update All Sources
# ============================================================================
if ($Update) {
$index = Get-SkillIndex
if (-not $index) { exit 1 }
Write-Host "🔄 Updating all sources..." -ForegroundColor Cyan
foreach ($src in $index.sources) {
if ($src.url -match "github\.com/([^/]+/[^/]+)") {
$repoFull = $Matches[1]
Write-Host "`n📦 $($src.id) ($repoFull)" -ForegroundColor White
$foundSkills = @()
# Method 1: Use GitHub Code Search API to find all SKILL.md files
try {
$rawOutput = gh api search/code -f "q=repo:$repoFull filename:SKILL.md" 2>$null
if ($LASTEXITCODE -eq 0 -and $rawOutput) {
$data = $rawOutput | ConvertFrom-Json
$items = $data.items
if ($items -and $items.Count -gt 0) {
$seenPaths = @{}
foreach ($item in $items) {
$path = $item.path
if ($path -and $path.EndsWith("SKILL.md")) {
# Get parent directory (skill folder)
$parts = $path -split "/"
$parent = ($parts[0..($parts.Count - 2)]) -join "/"
if ($parent -and -not $seenPaths.ContainsKey($parent)) {
$seenPaths[$parent] = $true
$skillName = $parts[$parts.Count - 2]
$foundSkills += @{ name = $skillName; path = $parent }
}
}
}
if ($foundSkills.Count -gt 0) {
Write-Host " 📂 Found $($foundSkills.Count) skills via Code Search" -ForegroundColor Green
foreach ($skill in $foundSkills) {
Write-Host " - $($skill.name) ($($skill.path))" -ForegroundColor Gray
}
}
}
}
}
catch {
Write-Host " ⚠️ Code Search failed, falling back to directory scan..." -ForegroundColor Yellow
}
# Method 2: Fallback to directory-based search if Code Search fails or returns empty
if ($foundSkills.Count -eq 0) {
$skillsPaths = @("skills", ".github/skills", ".claude/skills", "scientific-skills")
$foundInSubdir = $false
foreach ($path in $skillsPaths) {
try {
$rawOutput = gh api "repos/$repoFull/contents/$path" 2>$null
if ($LASTEXITCODE -eq 0 -and $rawOutput -and $rawOutput -notmatch '"message"') {
$items = $rawOutput | ConvertFrom-Json
if ($items) {
$foundInSubdir = $true
Write-Host " 📂 Found $(@($items).Count) items in $path" -ForegroundColor Green
foreach ($item in $items) {
if ($item.type -eq "dir") {
$foundSkills += @{ name = $item.name; path = "$path/$($item.name)" }
Write-Host " - $($item.name)" -ForegroundColor Gray
}
}
break
}
}
}
catch { }
}
# If no skills/ directory found, check root for SKILL.md in subdirectories
if (-not $foundInSubdir) {
try {
$rawOutput = gh api "repos/$repoFull/contents" 2>$null
if ($LASTEXITCODE -eq 0 -and $rawOutput) {
$items = $rawOutput | ConvertFrom-Json
$skillDirs = @()
$skipDirs = @(".", ".github", ".claude", "docs", "examples", "tests", "node_modules", "dist", "build", "src", "lib", "scripts")
foreach ($item in $items) {
if ($item.type -eq "dir" -and $item.name -and -not $item.name.StartsWith(".")) {
if ($skipDirs -notcontains $item.name) {
$skillMdCheck = gh api "repos/$repoFull/contents/$($item.name)/SKILL.md" 2>$null
if ($LASTEXITCODE -eq 0 -and $skillMdCheck -and $skillMdCheck -notmatch '"message"') {
$foundSkills += @{ name = $item.name; path = $item.name }
}
}
}
}
if ($foundSkills.Count -gt 0) {
Write-Host " 📂 Found $($foundSkills.Count) skills at root level" -ForegroundColor Green
foreach ($skill in $foundSkills) {
Write-Host " - $($skill.name)" -ForegroundColor Gray
}
}
}
}
catch { }
}
}
# Add found skills to index
foreach ($skill in $foundSkills) {
$existing = $index.skills | Where-Object { $_.name -eq $skill.name -and $_.source -eq $src.id }
if (-not $existing) {
$newSkill = [PSCustomObject]@{
name = $skill.name
source = $src.id
path = $skill.path
categories = @("community")
description = "$($skill.name) skill"
}
$index.skills += $newSkill
Write-Host " ✅ $($skill.name)" -ForegroundColor Green
} else {
Write-Host " ⏭️ $($skill.name) (exists)" -ForegroundColor Gray
}
}
}
}
Save-SkillIndex -Index $index
Write-Host "`n✅ Update complete!" -ForegroundColor Green
exit 0
}
# ============================================================================
# Add source
# ============================================================================
if ($AddSource) {
$index = Get-SkillIndex
if (-not $index) { exit 1 }
# Check URL
if (-not $RepoUrl) {
Write-Error "Please specify repository URL: -RepoUrl 'https://github.com/owner/repo'"
exit 1
}
# Parse owner/repo from URL
if ($RepoUrl -match "github\.com[/:]([^/]+)/([^/]+)") {
$owner = $Matches[1]
$repo = $Matches[2] -replace "\.git$", ""
$repoFullName = "$owner/$repo"
}
else {
Write-Error "Invalid GitHub URL: $RepoUrl"
exit 1
}
# Check if exists
$existingSource = $index.sources | Where-Object { $_.url -like "*$repoFullName*" }
if ($existingSource) {
Write-Warning "Source already exists: $($existingSource.id)"
exit 0
}
# Generate ID and name
$sourceId = $repo.ToLower() -replace "[^a-z0-9-]", "-"
$sourceName = $repo
$sourceType = "community"
$sourceDesc = "Skills from $owner/$repo"
# Add source
$newSource = [PSCustomObject]@{
id = $sourceId
name = $sourceName
url = "https://github.com/$repoFullName"
type = $sourceType
description = $sourceDesc
}
$index.sources += $newSource
Save-SkillIndex -Index $index
Write-Host "`n📦 Added new source:" -ForegroundColor Cyan
Write-Host " ID: $sourceId" -ForegroundColor White
Write-Host " URL: https://github.com/$repoFullName" -ForegroundColor Blue
# Fetch skills
Write-Host "`n🔍 Searching for skills..." -ForegroundColor Cyan
# Search for skills directory
$skillsPath = @("skills", ".github/skills", ".claude/skills")
$foundSkills = @()
foreach ($path in $skillsPath) {
try {
$rawOutput = gh api "repos/$repoFullName/contents/$path" 2>$null
if ($LASTEXITCODE -eq 0 -and $rawOutput -and $rawOutput -notmatch '"message"') {
$items = $rawOutput | ConvertFrom-Json | ForEach-Object { $_.name }
if ($items) {
Write-Host " 📂 Found $(@($items).Count) skills in $path" -ForegroundColor Green
foreach ($skillName in $items) {
$foundSkills += @{ name = $skillName; path = "$path/$skillName" }
Write-Host " - $skillName" -ForegroundColor Gray
}
break
}
}
}
catch { }
}
if ($foundSkills.Count -eq 0) {
Write-Host " ⚠️ No skills directory found" -ForegroundColor Yellow
}
# Add found skills
if ($foundSkills.Count -gt 0) {
Write-Host "`n✨ Adding skills to index..." -ForegroundColor Cyan
foreach ($skill in $foundSkills) {
$existingSkill = $index.skills | Where-Object { $_.name -eq $skill.name -and $_.source -eq $sourceId }
if (-not $existingSkill) {
$newSkill = [PSCustomObject]@{
name = $skill.name
source = $sourceId
path = $skill.path
categories = @("community")
description = "$($skill.name) skill"
}
$index.skills += $newSkill
Write-Host " ✅ $($skill.name)" -ForegroundColor Green
}
else {
Write-Host " ⏭️ $($skill.name) (exists)" -ForegroundColor Gray
}
}
Save-SkillIndex -Index $index
}
exit 0
}
# カテゴリ一覧を表示
if ($ListCategories) {
$index = Get-SkillIndex
if ($index) {
Write-Host "`n📁 利用可能なカテゴリ:" -ForegroundColor Cyan
$index.categories | ForEach-Object {
Write-Host " $($_.id)" -ForegroundColor White -NoNewline
Write-Host " - $($_.description)" -ForegroundColor Gray
}
}
exit 0
}
# ソース一覧を表示
if ($ListSources) {
$index = Get-SkillIndex
if ($index) {
Write-Host "`n📦 利用可能なソース:" -ForegroundColor Cyan
$index.sources | ForEach-Object {
Write-Host " $($_.id)" -ForegroundColor White -NoNewline
Write-Host " [$($_.type)]" -ForegroundColor Yellow -NoNewline
Write-Host " - $($_.name)" -ForegroundColor Gray
Write-Host " $($_.url)" -ForegroundColor DarkCyan
}
}
exit 0
}
# インデックス更新
if ($UpdateIndex) {
Write-Host "🔄 インデックスを更新中..." -ForegroundColor Cyan
$sources = @(
@{ id = "anthropics-skills"; repo = "anthropics/skills"; path = "skills" },
@{ id = "obra-superpowers"; repo = "obra/superpowers"; path = "skills" },
@{ id = "composio-awesome"; repo = "ComposioHQ/awesome-claude-skills"; path = "" }
)
foreach ($src in $sources) {
Write-Host " 📥 $($src.repo) からスキルを取得中..." -ForegroundColor Gray
try {
$apiPath = if ($src.path) { "repos/$($src.repo)/contents/$($src.path)" } else { "repos/$($src.repo)/contents" }
$items = gh api $apiPath --jq '.[].name' 2>$null
if ($items) {
Write-Host " ✅ $(@($items).Count) 件取得" -ForegroundColor Green
}
}
catch {
Write-Warning " ⚠️ 取得失敗: $($_.Exception.Message)"
}
}
Write-Host "`n💡 手動で references/skill-index.json を編集してスキルを追加してください。" -ForegroundColor Yellow
exit 0
}
# ローカル検索 (タグ検索対応)
function Search-LocalIndex {
param([string]$Query, [string]$Category, [string]$Source)
$index = Get-SkillIndex
if (-not $index) { return @() }
$results = $index.skills
$starred = Get-StarredSkills
# タグ抽出 (#azure #bicep など)
$tags = @()
$cleanQuery = $Query
if ($Query -match '#(\w+)') {
$tags = [regex]::Matches($Query, '#(\w+)') | ForEach-Object { $_.Groups[1].Value }
$cleanQuery = ($Query -replace '#\w+', '').Trim()
}
# キーワードフィルタ
if ($cleanQuery) {
$results = $results | Where-Object {
$_.name -like "*$cleanQuery*" -or $_.description -like "*$cleanQuery*"
}
}
# タグフィルタ (カテゴリとマッチ)
if ($tags.Count -gt 0) {
$results = $results | Where-Object {
$skillCats = $_.categories | ForEach-Object { $_.ToLower() }
$matchedTags = $tags | Where-Object { $skillCats -contains $_.ToLower() }
$matchedTags.Count -gt 0
}
}
# カテゴリフィルタ
if ($Category) {
$results = $results | Where-Object {
$_.categories -contains $Category
}
}
# ソースフィルタ
if ($Source) {
$results = $results | Where-Object {
$_.source -eq $Source
}
}
# ソース情報とスター状態を付加
$results | ForEach-Object {
$skill = $_
$sourceInfo = $index.sources | Where-Object { $_.id -eq $skill.source }
$skillId = "$($skill.source)/$($skill.name)"
$skill | Add-Member -NotePropertyName "sourceUrl" -NotePropertyValue $sourceInfo.url -Force
$skill | Add-Member -NotePropertyName "sourceName" -NotePropertyValue $sourceInfo.name -Force
$skill | Add-Member -NotePropertyName "starred" -NotePropertyValue ($starred -contains $skillId) -Force
}
# スター付きを先頭に
$results = $results | Sort-Object -Property @{Expression={$_.starred}; Descending=$true}, name
return $results
}
# 外部検索 (GitHub)
function Search-External {
param([string]$Query)
Write-Host "`n🌐 GitHub を検索中..." -ForegroundColor Cyan
$searchQuery = if ($Query) { "$Query filename:SKILL.md" } else { "filename:SKILL.md path:.github/skills" }
try {
$results = gh search code $searchQuery --json repository,path,url --limit 15 2>&1 | ConvertFrom-Json
return $results
}
catch {
Write-Warning "GitHub 検索に失敗しました: $($_.Exception.Message)"
return @()
}
}
# Web 検索 (最終フォールバック)
function Search-Web {
param([string]$Query, [switch]$OpenBrowser)
$searchTerms = if ($Query) {
"claude skill $Query OR copilot skill $Query SKILL.md"
} else {
"claude skills SKILL.md github"
}
$encodedQuery = [System.Web.HttpUtility]::UrlEncode($searchTerms)
# 各検索エンジンの URL
$urls = @{
"Google" = "https://www.google.com/search?q=$encodedQuery"
"Bing" = "https://www.bing.com/search?q=$encodedQuery"
"DuckDuckGo" = "https://duckduckgo.com/?q=$encodedQuery"
}
Write-Host "`n🔎 Web 検索 URL:" -ForegroundColor Cyan
foreach ($engine in $urls.Keys) {
Write-Host " $engine : " -ForegroundColor Gray -NoNewline
Write-Host $urls[$engine] -ForegroundColor Blue
}
if ($OpenBrowser) {
Write-Host "`n ブラウザで Google 検索を開きます..." -ForegroundColor Yellow
Start-Process $urls["Google"]
}
return $urls
}
# メイン検索処理
Write-Host "`n🔍 スキルを検索中..." -ForegroundColor Cyan
# インデックス読み込み
$index = Get-SkillIndex
if (-not $index) { exit 1 }
# 自動更新チェック(インタラクティブモード時のみ)
if (-not $NoInteractive) {
$index = Invoke-AutoUpdateCheck -Index $index
}
# 1. ローカル検索
$localResults = Search-LocalIndex -Query $Query -Category $Category -Source $Source
if ($localResults -and $localResults.Count -gt 0) {
Write-Host "`n📋 ローカルインデックスから $($localResults.Count) 件見つかりました:" -ForegroundColor Green
$i = 1
foreach ($skill in $localResults) {
$starMark = if ($skill.starred) { " ⭐" } else { "" }
Write-Host "`n[$i] " -ForegroundColor Yellow -NoNewline
Write-Host "$($skill.name)$starMark" -ForegroundColor White
Write-Host " 📝 $($skill.description)" -ForegroundColor Gray
Write-Host " 📦 $($skill.sourceName)" -ForegroundColor DarkCyan
Write-Host " 🏷️ $($skill.categories -join ', ')" -ForegroundColor DarkGray
Write-Host " 🔗 $($skill.sourceUrl)/$($skill.path)" -ForegroundColor Blue
$i++
}
}
else {
Write-Host " ローカルインデックスに該当なし" -ForegroundColor Yellow
}
# 2. 外部検索 (ローカルで見つからない、または強制検索)
$externalFound = $false
if ((-not $localResults -or $localResults.Count -eq 0) -or $SearchExternal) {
$externalResults = Search-External -Query $Query
if ($externalResults -and $externalResults.Count -gt 0) {
$externalFound = $true
Write-Host "`n🌐 GitHub から $($externalResults.Count) 件見つかりました:" -ForegroundColor Green
$i = 1
foreach ($item in $externalResults) {
$repoName = $item.repository.nameWithOwner
Write-Host "`n[$i] " -ForegroundColor Yellow -NoNewline
Write-Host $repoName -ForegroundColor White
Write-Host " 📄 $($item.path)" -ForegroundColor Gray
Write-Host " 🔗 https://github.com/$repoName" -ForegroundColor Blue
$i++
}
Write-Host "`n💡 ヒント: -AddSource でリポジトリをインデックスに追加できます" -ForegroundColor Yellow
}
else {
Write-Host "`n GitHub でも見つかりませんでした" -ForegroundColor Yellow
}
}
# 3. Web 検索 (ローカルも GitHub も見つからない場合、または -SearchWeb 指定時)
$totalFound = ($localResults.Count -gt 0) -or $externalFound
if ((-not $totalFound -and $Query) -or $SearchWeb) {
Write-Host "`n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor DarkGray
if (-not $totalFound) {
Write-Host "📭 ローカル・GitHub ともに見つかりませんでした。" -ForegroundColor Yellow
}
Write-Host " Web 検索を試してみてください:" -ForegroundColor Yellow
Search-Web -Query $Query -OpenBrowser:$SearchWeb | Out-Null
}
# 4. 類似スキル提案 (結果が少ない場合)
if ($Query -and $localResults.Count -lt 3) {
$similar = $index.skills | Where-Object {
$_.name -like "*$Query*" -and $_ -notin $localResults
} | Select-Object -First 3
if ($similar) {
Write-Host "`n💡 こちらもおすすめ:" -ForegroundColor Cyan
foreach ($s in $similar) {
Write-Host " - $($s.name)" -ForegroundColor Gray
}
}
}
# 5. 検索後のサジェスト表示
if ($Query) {
Show-PostSearchSuggestions -Index $index -Query $Query -Results $localResults
}
# 6. 他のリポジトリを探すか聞く(ローカル結果が少ない場合、インタラクティブモード時のみ)
if ($Query -and $localResults.Count -lt 5 -and (-not $SearchExternal) -and (-not $NoInteractive)) {
Invoke-DiscoverNewRepos -Query $Query
}
Write-Host ""
#!/usr/bin/env python3
"""
Fetch descriptions from various GitHub repositories and update skill-index.json
"""
import json
import subprocess
import re
import time
from pathlib import Path
# Paths
SCRIPT_DIR = Path(__file__).parent
INDEX_PATH = SCRIPT_DIR / ".." / "references" / "skill-index.json"
# Source configurations: (source_id, owner, repo, skill_path_prefix)
SOURCES = [
("composio-awesome", "ComposioHQ", "awesome-claude-skills", ""),
("claude-code-harness", "Chachamaru127", "claude-code-harness", "skills"),
("claude-command-control", "enuno", "claude-command-and-control", "skills"),
("claude-plugins-plus", "jeremylongshore", "claude-code-plugins-plus-skills", "skills"),
("aktsmm-agent-skills", "aktsmm", "Agent-Skills", ""),
]
def get_skill_description(owner: str, repo: str, skill_path: str) -> str | None:
"""Fetch description from SKILL.md frontmatter"""
try:
# Try SKILL.md first
result = subprocess.run(
[
"gh", "api",
f"repos/{owner}/{repo}/contents/{skill_path}/SKILL.md",
"-H", "Accept: application/vnd.github.raw"
],
capture_output=True,
timeout=30,
encoding='utf-8',
errors='replace'
)
if result.returncode != 0:
# Try README.md as fallback
result = subprocess.run(
[
"gh", "api",
f"repos/{owner}/{repo}/contents/{skill_path}/README.md",
"-H", "Accept: application/vnd.github.raw"
],
capture_output=True,
timeout=30,
encoding='utf-8',
errors='replace'
)
if result.returncode != 0:
return None
content = result.stdout
# Extract description from YAML frontmatter
match = re.search(r'^description:\s*["\']?(.+?)["\']?\s*$', content, re.MULTILINE)
if match:
desc = match.group(1).strip()
# Remove trailing quote if present
desc = desc.rstrip('"\'')
# Truncate if too long
if len(desc) > 100:
desc = desc[:97] + "..."
return desc
# Fallback: try to get first sentence after # heading
match = re.search(r'^#\s+.+?\n+(.+?)\.', content, re.MULTILINE)
if match:
desc = match.group(1).strip()
if len(desc) > 100:
desc = desc[:97] + "..."
return desc
return None
except subprocess.TimeoutExpired:
return None
except Exception as e:
print(f" Error: {e}")
return None
def main():
# Load index
with open(INDEX_PATH, "r", encoding="utf-8") as f:
index = json.load(f)
# Create source lookup
source_lookup = {s["id"]: s for s in index["sources"]}
updated = 0
skipped = 0
for skill in index["skills"]:
current_desc = skill.get("description", "")
# Skip if already has a good description
if current_desc and not current_desc.endswith(" skill"):
skipped += 1
continue
source_id = skill.get("source")
source = source_lookup.get(source_id)
if not source:
continue
# Parse owner/repo from source URL
url = source.get("url", "")
match = re.search(r'github\.com/([^/]+)/([^/]+)', url)
if not match:
continue
owner, repo = match.groups()
skill_path = skill.get("path", skill["name"])
print(f"Fetching: {source_id}/{skill['name']}...")
desc = get_skill_description(owner, repo, skill_path)
if desc:
skill["description"] = desc
print(f" OK: {desc[:60]}...")
updated += 1
else:
print(f" SKIP: {current_desc}")
# Rate limiting
time.sleep(0.3)
# Save updated index
with open(INDEX_PATH, "w", encoding="utf-8") as f:
json.dump(index, f, indent=2, ensure_ascii=False)
print(f"\nUpdated {updated} skills, skipped {skipped}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Fetch descriptions from K-Dense-AI/claude-scientific-skills SKILL.md files
and update skill-index.json
"""
import json
import subprocess
import re
import time
from pathlib import Path
# Paths
SCRIPT_DIR = Path(__file__).parent
INDEX_PATH = SCRIPT_DIR / ".." / "references" / "skill-index.json"
def get_skill_description(skill_name: str) -> str | None:
"""Fetch description from SKILL.md frontmatter"""
try:
result = subprocess.run(
[
"gh", "api",
f"repos/K-Dense-AI/claude-scientific-skills/contents/scientific-skills/{skill_name}/SKILL.md",
"-H", "Accept: application/vnd.github.raw"
],
capture_output=True,
timeout=30,
encoding='utf-8',
errors='replace'
)
if result.returncode != 0:
print(f" ⚠️ Failed to fetch {skill_name}: {result.stderr[:100]}")
return None
content = result.stdout
# Extract description from YAML frontmatter
# Pattern: description: "..." or description: '...' or description: ...
match = re.search(r'^description:\s*["\']?(.+?)["\']?\s*$', content, re.MULTILINE)
if match:
desc = match.group(1).strip()
# Truncate if too long
if len(desc) > 100:
desc = desc[:97] + "..."
return desc
return None
except subprocess.TimeoutExpired:
print(f" ⚠️ Timeout fetching {skill_name}")
return None
except Exception as e:
print(f" ⚠️ Error fetching {skill_name}: {e}")
return None
def main():
# Load index
with open(INDEX_PATH, "r", encoding="utf-8") as f:
index = json.load(f)
# Find all claude-scientific-skills skills
updated = 0
skipped = 0
for skill in index["skills"]:
if skill.get("source") != "claude-scientific-skills":
continue
name = skill["name"]
current_desc = skill.get("description", "")
# Skip if already has a good description (not "xxx skill" pattern)
if current_desc and not current_desc.endswith(" skill"):
skipped += 1
continue
print(f"Fetching: {name}...")
desc = get_skill_description(name)
if desc:
skill["description"] = desc
# Also update categories based on description keywords
if "community" in skill.get("categories", []):
skill["categories"] = infer_categories(name, desc)
print(f" OK: {desc[:60]}...")
updated += 1
else:
print(f" SKIP: {current_desc}")
# Rate limiting - 1 request per 0.5 seconds
time.sleep(0.5)
# Save updated index
with open(INDEX_PATH, "w", encoding="utf-8") as f:
json.dump(index, f, indent=2, ensure_ascii=False)
print(f"\nUpdated {updated} skills, skipped {skipped}")
def infer_categories(name: str, desc: str) -> list[str]:
"""Infer categories from skill name and description"""
categories = ["science"]
text = (name + " " + desc).lower()
if any(kw in text for kw in ["bio", "protein", "dna", "rna", "gene", "cell", "molecular", "sequence"]):
categories.append("biology")
if any(kw in text for kw in ["chem", "molecule", "compound", "drug"]):
categories.append("chemistry")
if any(kw in text for kw in ["data", "analysis", "visualization", "plot", "statistics"]):
categories.append("data-science")
if any(kw in text for kw in ["database", "db", "query"]):
categories.append("database")
if any(kw in text for kw in ["machine learning", "ml", "deep", "neural", "ai"]):
categories.append("ml")
if any(kw in text for kw in ["quantum", "physics"]):
categories.append("physics")
if any(kw in text for kw in ["clinical", "medical", "patient", "health"]):
categories.append("medical")
if any(kw in text for kw in ["paper", "publication", "citation", "literature"]):
categories.append("research")
return categories
if __name__ == "__main__":
main()
Related skills
How it compares
Choose skill-finder over generic repo search when you need SKILL.md-specific discovery, install steps, and primitive-selection guidance in one workflow.
FAQ
What does skill-finder search?
skill-finder searches GitHub repositories and local filesystem paths for Agent Skills packaged as SKILL.md modules. Developers supply task keywords or domain hints, then skill-finder evaluates candidates for install and ongoing collection management.
When should I pick a skill over a prompt?
skill-finder helps decide whether a reusable SKILL.md module, a one-off prompt, an instruction file, a hook, or a dedicated agent fits the task. Skills suit repeatable domain workflows; prompts suit single-turn tasks without filesystem packaging.