@cangurel81/skill-router 0.0.0-stage → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +253 -2
- package/bin/skill-router.mjs +420 -0
- package/package.json +44 -4
- package/skills/skill-router/SKILL.md +109 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Can Gurel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,254 @@
|
|
|
1
|
-
#
|
|
1
|
+
# skill-router
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/cangurel81/skill-router/actions/workflows/test.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@cangurel81/skill-router)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](package.json)
|
|
7
|
+
|
|
8
|
+
Index, classify and search a large agent-skill catalogue — so the agent stops guessing which of
|
|
9
|
+
your 900 skills applies, and so you notice when most of the catalogue is duplicated.
|
|
10
|
+
|
|
11
|
+
Zero dependencies. Node 18+. One file. No config required.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx @cangurel81/skill-router --search expo
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## The problem it solves
|
|
20
|
+
|
|
21
|
+
Once a machine has more than a few dozen installed skills, two failures appear and neither one is
|
|
22
|
+
visible:
|
|
23
|
+
|
|
24
|
+
**1. The agent guesses.** It names a skill that does not exist, or writes from scratch something
|
|
25
|
+
already installed forty times over, because it cannot tell which of a thousand descriptions
|
|
26
|
+
matches the task.
|
|
27
|
+
|
|
28
|
+
**2. The catalogue silently fills with copies.** A repository ships `skills/` plus
|
|
29
|
+
`docs/ja-JP/skills`, `docs/zh-CN/skills` and a mirrored `pi/core/skills` tree. Every one is
|
|
30
|
+
discovered as a separate capability. Your inventory says 966 skills; 651 of them are the same 315
|
|
31
|
+
translated or mirrored. Every search returns duplicates, and the counts you reason from are wrong.
|
|
32
|
+
|
|
33
|
+
`skill-router` measures both and answers the routing question with paths, not vibes.
|
|
34
|
+
|
|
35
|
+
## What it does
|
|
36
|
+
|
|
37
|
+
| Step | Detail |
|
|
38
|
+
|---|---|
|
|
39
|
+
| Finds roots | `--root` flags, `SKILL_ROOTS`, or well-known install locations |
|
|
40
|
+
| Walks | up to 5 levels deep, skips `node_modules`, `.git`, `dist`, `build`, `vendor` |
|
|
41
|
+
| Parses | `SKILL.md` YAML frontmatter without a YAML dependency (block scalars included) |
|
|
42
|
+
| Classifies | 13 domain groups by weighted keyword match on name + description |
|
|
43
|
+
| De-duplicates | two detectors: `docs/<locale>/` trees, and byte-identical files via SHA-1 |
|
|
44
|
+
| Reports | inventory, per-domain counts, duplicate names, unusable descriptions |
|
|
45
|
+
| Searches | ranked matches, word-boundary aware, with full file paths |
|
|
46
|
+
|
|
47
|
+
It never edits, deletes or installs anything. It reads and reports.
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
Pick one. They all run the same three files.
|
|
52
|
+
|
|
53
|
+
**npm — recommended**
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npx @cangurel81/skill-router --root ~/.claude/skills --search testing # no install
|
|
57
|
+
npm i -g @cangurel81/skill-router # global command
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The package installs the command `skill-router`, so after a global install you can drop the
|
|
61
|
+
`npx @cangurel81/` prefix:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
skill-router --search expo
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Clone and run directly**
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
git clone https://github.com/cangurel81/skill-router.git
|
|
71
|
+
cd skill-router
|
|
72
|
+
node bin/skill-router.mjs
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Use as an agent skill**
|
|
76
|
+
|
|
77
|
+
Register the repo's `skills/` folder as a skill root, in whichever harness you use:
|
|
78
|
+
|
|
79
|
+
```jsonc
|
|
80
|
+
// opencode.jsonc
|
|
81
|
+
{ "skills": ["~/code/skill-router/skills"] }
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# Claude Code
|
|
86
|
+
ln -s ~/code/skill-router/skills/skill-router ~/.claude/skills/skill-router
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The skill file at `skills/skill-router/SKILL.md` teaches the agent the routing procedure: name the
|
|
90
|
+
domain, search two or three words, read the path, then open only the winner.
|
|
91
|
+
|
|
92
|
+
**Copy the script**
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
curl -O https://raw.githubusercontent.com/cangurel81/skill-router/main/bin/skill-router.mjs
|
|
96
|
+
node skill-router.mjs --root ~/.claude/skills --search testing
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Usage
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
node bin/skill-router.mjs # inventory of every root
|
|
103
|
+
node bin/skill-router.mjs --search expo # ranked matches
|
|
104
|
+
node bin/skill-router.mjs --search video --group video
|
|
105
|
+
node bin/skill-router.mjs --groups # domain keys and counts
|
|
106
|
+
node bin/skill-router.mjs --dupes # same name, different folders
|
|
107
|
+
node bin/skill-router.mjs --weak # descriptions too short to route on
|
|
108
|
+
node bin/skill-router.mjs --json # machine-readable, all records
|
|
109
|
+
node bin/skill-router.mjs --write ./index # write index/skills.json
|
|
110
|
+
node bin/skill-router.mjs --help
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Exit codes: `0` success, `1` no skill root found.
|
|
114
|
+
|
|
115
|
+
### Choosing roots
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
node bin/skill-router.mjs --root ./skills --root ~/.claude/skills
|
|
119
|
+
SKILL_ROOTS=~/.claude/skills:./skills node bin/skill-router.mjs # ';' on Windows
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`--root` wins over `SKILL_ROOTS`, which wins over the defaults:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
./.agents/skills ./.opencode/skill ./.claude/skills ./skills
|
|
126
|
+
~/.claude/skills ~/.codex/skills ~/.kimi-code/skills
|
|
127
|
+
~/.config/opencode/skills ~/.config/opencode/local-skills
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Real output
|
|
131
|
+
|
|
132
|
+
Run against a real repository that registers its root instead of its `skills/` folder:
|
|
133
|
+
|
|
134
|
+
```console
|
|
135
|
+
$ node bin/skill-router.mjs --root ~/.config/opencode/vendor/affaan-m__ecc
|
|
136
|
+
|
|
137
|
+
SKILL CATALOGUE
|
|
138
|
+
================================================================
|
|
139
|
+
Roots with skills : 1
|
|
140
|
+
/home/me/.config/opencode/vendor/affaan-m__ecc
|
|
141
|
+
966 files, 315 genuine
|
|
142
|
+
|
|
143
|
+
Total SKILL.md files : 966
|
|
144
|
+
genuine : 315
|
|
145
|
+
copies : 651 (67%) [locale 518, identical 133]
|
|
146
|
+
unique names (genuine) : 315
|
|
147
|
+
duplicate names : 0
|
|
148
|
+
weak descriptions : 0
|
|
149
|
+
|
|
150
|
+
By group (genuine only):
|
|
151
|
+
Agent / LLM / orchestration 105
|
|
152
|
+
Other 45
|
|
153
|
+
Code quality / testing / process 30
|
|
154
|
+
Backend / API / databases 28
|
|
155
|
+
Mobile / Expo / React Native 20
|
|
156
|
+
Frontend / UI / design systems 18
|
|
157
|
+
Infra / DevOps / network 16
|
|
158
|
+
Marketing / content / social 12
|
|
159
|
+
Video / motion / audio 12
|
|
160
|
+
Data / research / analytics 10
|
|
161
|
+
Security / compliance 9
|
|
162
|
+
Desktop / documents / office 7
|
|
163
|
+
SEO / GEO / search 3
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The honest reading: 315 real capabilities, 651 files that only exist because the same skills were
|
|
167
|
+
translated into six languages and mirrored once. Register `.../ECC-main/skills` instead of
|
|
168
|
+
`.../ECC-main` and 966 becomes 315.
|
|
169
|
+
|
|
170
|
+
Searching is where it earns its place:
|
|
171
|
+
|
|
172
|
+
```console
|
|
173
|
+
$ node bin/skill-router.mjs --search caption
|
|
174
|
+
|
|
175
|
+
"caption" -> 1 match(es)
|
|
176
|
+
|
|
177
|
+
* remotion-video-creation [Video / motion / audio]
|
|
178
|
+
Best practices for Remotion - Video creation in React. 29 domain-specific rules covering 3D,
|
|
179
|
+
animations, audio, captions, charts, transitions, and more...
|
|
180
|
+
/home/me/.config/opencode/vendor/affaan-m__ecc/ECC-main/skills/remotion-video-creation/SKILL.md
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The path is not decoration. Two skills can share a name and differ completely; the path is the only
|
|
184
|
+
thing that says which repository you are actually loading.
|
|
185
|
+
|
|
186
|
+
## Reading the report
|
|
187
|
+
|
|
188
|
+
Every number has an action attached:
|
|
189
|
+
|
|
190
|
+
| Signal | Means | Do this |
|
|
191
|
+
|---|---|---|
|
|
192
|
+
| `copies` above 10% | a root points at a repository, not at its `skills/` folder | point it at the real skill folder |
|
|
193
|
+
| `duplicate names` high | two repos ship the same skill; the agent may load either | drop one root, or accept the newer repo |
|
|
194
|
+
| `weak descriptions` | description under 25 chars, cannot be routed to | extend it, or treat the skill as absent |
|
|
195
|
+
| `Other` group large | descriptions too vague to classify | the same skills are also unroutable |
|
|
196
|
+
| a domain at `0` | genuine capability gap | this is where a new skill is justified |
|
|
197
|
+
|
|
198
|
+
`--dupes` prints both paths for every collision, so you can diff them before deciding.
|
|
199
|
+
`--weak` prints the character count, so "too short" is a number and not an opinion.
|
|
200
|
+
|
|
201
|
+
## When a new skill is justified
|
|
202
|
+
|
|
203
|
+
All three must hold:
|
|
204
|
+
|
|
205
|
+
1. `--search` over three plausible terms returns nothing relevant.
|
|
206
|
+
2. The gap is knowledge no public repository could ship — local toolchain, local traps, private
|
|
207
|
+
systems.
|
|
208
|
+
3. The knowledge repeats. It has already cost time twice.
|
|
209
|
+
|
|
210
|
+
Otherwise extend an existing skill or fix its description. A fourth near-duplicate makes the
|
|
211
|
+
catalogue worse, not more capable.
|
|
212
|
+
|
|
213
|
+
## How duplicate detection works
|
|
214
|
+
|
|
215
|
+
Two independent detectors, because the two failure modes look different:
|
|
216
|
+
|
|
217
|
+
**Locale trees.** Any path matching `docs/<lang>/…` where `<lang>` is a locale slug (`tr`, `ja-JP`,
|
|
218
|
+
`zh-CN`, `pt_BR`) is a translated copy. This catches translations, which are *not* byte-identical
|
|
219
|
+
and would otherwise look like 183 new skills.
|
|
220
|
+
|
|
221
|
+
**Identical files.** Skills sharing a name whose `SKILL.md` has the same SHA-1 are the same file
|
|
222
|
+
copied. This catches mirrored trees (`pi/core/skills`, a vendored second copy) with no naming
|
|
223
|
+
convention to key on. The copy inside a real `skills/` directory wins as canonical, then the
|
|
224
|
+
shortest path, then lexicographic order — so the reported path never sends you to the mirror, and
|
|
225
|
+
the result is stable across runs and platforms.
|
|
226
|
+
|
|
227
|
+
Nothing is auto-deleted. Deleting skill folders is a decision with consequences; the tool hands you
|
|
228
|
+
the list and the path.
|
|
229
|
+
|
|
230
|
+
## Development
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
npm test # syntax check + smoke test
|
|
234
|
+
node test/smoke.mjs
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
The smoke test builds a throwaway catalogue containing one genuine pair, one translated locale
|
|
238
|
+
copy and one byte-identical mirror, runs the real CLI, and asserts the counts, the canonical path,
|
|
239
|
+
the miss path and the exit codes. CI runs it on Linux, macOS and Windows across Node 18, 20 and 22 —
|
|
240
|
+
the script must behave identically on all three, since path separators are the main portability
|
|
241
|
+
risk here.
|
|
242
|
+
|
|
243
|
+
## Limits
|
|
244
|
+
|
|
245
|
+
- It indexes discoverability, not quality. A short description is a routing failure, not a bad
|
|
246
|
+
skill.
|
|
247
|
+
- It reads frontmatter only. It does not open skill bodies — that is a separate review pass.
|
|
248
|
+
- Classification is keyword-based and will occasionally land a skill in the wrong group. Search
|
|
249
|
+
covers all groups by default, so this only costs you when you filter with `--group`.
|
|
250
|
+
- It does not fetch, install or update skills.
|
|
251
|
+
|
|
252
|
+
## License
|
|
253
|
+
|
|
254
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* skill-router - index, classify and search a large agent-skill catalogue.
|
|
4
|
+
*
|
|
5
|
+
* Once a machine has more than a few dozen skills, two things break:
|
|
6
|
+
* the agent cannot tell which skill applies, and nobody notices when the
|
|
7
|
+
* catalogue is half full of duplicated or translated copies.
|
|
8
|
+
*
|
|
9
|
+
* This tool scans every skill root, reads each SKILL.md frontmatter, groups
|
|
10
|
+
* skills by domain, separates real capabilities from locale/duplicate trees,
|
|
11
|
+
* and answers "which skill fits this job?" with ranked matches and paths.
|
|
12
|
+
*
|
|
13
|
+
* Zero dependencies. Node 18+.
|
|
14
|
+
*
|
|
15
|
+
* node bin/skill-router.mjs summary of every root
|
|
16
|
+
* node bin/skill-router.mjs --search expo ranked matches
|
|
17
|
+
* node bin/skill-router.mjs --search video --group video
|
|
18
|
+
* node bin/skill-router.mjs --groups group keys and counts
|
|
19
|
+
* node bin/skill-router.mjs --dupes same name, different folders
|
|
20
|
+
* node bin/skill-router.mjs --weak unusable descriptions
|
|
21
|
+
* node bin/skill-router.mjs --json machine-readable output
|
|
22
|
+
*
|
|
23
|
+
* Roots come from, in order:
|
|
24
|
+
* 1. --root <path> (repeatable, wins over everything)
|
|
25
|
+
* 2. SKILL_ROOTS env var (paths separated by the OS path delimiter)
|
|
26
|
+
* 3. well-known locations (see DEFAULT_ROOTS)
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { readFileSync, existsSync, readdirSync, statSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
30
|
+
import { createHash } from 'node:crypto';
|
|
31
|
+
import { join, dirname, basename, resolve, delimiter } from 'node:path';
|
|
32
|
+
import { homedir } from 'node:os';
|
|
33
|
+
|
|
34
|
+
const HOME = homedir();
|
|
35
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', '.cache', 'cache', 'dist', 'build', 'vendor']);
|
|
36
|
+
const MAX_DEPTH = 5;
|
|
37
|
+
const WEAK_DESCRIPTION = 25;
|
|
38
|
+
|
|
39
|
+
const DEFAULT_ROOTS = [
|
|
40
|
+
join(process.cwd(), '.agents', 'skills'),
|
|
41
|
+
join(process.cwd(), '.opencode', 'skill'),
|
|
42
|
+
join(process.cwd(), '.claude', 'skills'),
|
|
43
|
+
join(process.cwd(), 'skills'),
|
|
44
|
+
join(HOME, '.config', 'opencode', 'skills'),
|
|
45
|
+
join(HOME, '.config', 'opencode', 'local-skills'),
|
|
46
|
+
join(HOME, '.config', 'opencode', 'skill'),
|
|
47
|
+
join(HOME, '.claude', 'skills'),
|
|
48
|
+
join(HOME, '.codex', 'skills'),
|
|
49
|
+
join(HOME, '.kimi-code', 'skills'),
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
/* ------------------------------------------------------------------ groups */
|
|
53
|
+
|
|
54
|
+
const GROUPS = [
|
|
55
|
+
{ key: 'seo', label: 'SEO / GEO / search', words: ['seo', 'serp', 'backlink', 'hreflang', 'sitemap', 'schema markup', 'keyword research', 'ai overview', 'citability', 'llms.txt', 'local seo', 'search console', 'core web vitals'] },
|
|
56
|
+
{ key: 'video', label: 'Video / motion / audio', words: ['video', 'remotion', 'animation', 'motion graphic', 'ffmpeg', 'caption', 'subtitle', 'lottie', 'audiogram', 'music', 'text-to-speech', 'tts', 'voiceover', 'shorts', 'reels', 'waveform', 'slideshow', 'lower third', 'countdown'] },
|
|
57
|
+
{ key: 'wordpress', label: 'WordPress / WooCommerce', words: ['wordpress', 'woocommerce', 'wp-cli', 'elementor', 'gutenberg', 'custom post type', 'plugin header'] },
|
|
58
|
+
{ key: 'mobile', label: 'Mobile / Expo / React Native', words: ['expo', 'react native', 'android', 'ios', 'swift', 'kotlin', 'flutter', 'dart', 'app store', 'play store', 'eas build'] },
|
|
59
|
+
{ key: 'frontend', label: 'Frontend / UI / design systems', words: ['react', 'next.js', 'vue', 'nuxt', 'svelte', 'angular', 'css', 'tailwind', 'frontend', 'design system', 'accessibility', 'a11y', 'gsap', 'three.js', 'd3.js', 'component library'] },
|
|
60
|
+
{ key: 'backend', label: 'Backend / API / databases', words: ['laravel', 'symfony', 'spring boot', 'quarkus', 'mybatis', 'jpa', 'hibernate', 'php', 'django', 'fastapi', 'nestjs', 'express', 'rails', 'api design', 'rest api', 'graphql', 'microservice', 'postgres', 'mysql', 'redis', 'database', 'orm', 'query optimization', 'schema design'] },
|
|
61
|
+
{ key: 'marketing', label: 'Marketing / content / social', words: ['marketing', 'content strategy', 'social media', 'linkedin', 'twitter', 'newsletter', 'copywriting', 'brand voice', 'campaign', 'carousel', 'content brief', 'blog post', 'outreach', 'sales'] },
|
|
62
|
+
{ key: 'agent', label: 'Agent / LLM / orchestration', words: ['agent', 'llm', 'prompt', 'mcp', 'subagent', 'orchestrat', 'harness', 'eval', 'rag', 'memory', 'autonomous', 'skill', 'claude code', 'context window'] },
|
|
63
|
+
{ key: 'security', label: 'Security / compliance', words: ['security', 'vulnerab', 'pentest', 'hipaa', 'gdpr', 'compliance', 'phi', 'secret', 'threat model', 'access control'] },
|
|
64
|
+
{ key: 'infra', label: 'Infra / DevOps / network', words: ['docker', 'kubernetes', 'deploy', 'ci/cd', 'github actions', 'nginx', 'network', 'vlan', 'dns', 'vpn', 'homelab', 'wireguard', 'cloud'] },
|
|
65
|
+
{ key: 'data', label: 'Data / research / analytics', words: ['data pipeline', 'analytics', 'research', 'scrap', 'crawl', 'etl', 'sql', 'pandas', 'statistic', 'dashboard', 'benchmark'] },
|
|
66
|
+
{ key: 'code', label: 'Code quality / testing / process', words: ['test-driven', 'unit test', 'tdd', 'code review', 'debug', 'refactor', 'coding standard', 'typescript', 'python', 'golang', 'rust', 'java', 'lint', 'codebase', 'architecture decision'] },
|
|
67
|
+
{ key: 'desktop', label: 'Desktop / documents / office', words: ['windows', 'desktop', 'pywinauto', 'uiautomation', 'screenshot', 'excel', 'pdf', 'pandoc', 'document', 'slides', 'presentation'] },
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
function classify(name, description) {
|
|
71
|
+
const text = `${name} ${description}`.toLowerCase();
|
|
72
|
+
let best = null;
|
|
73
|
+
let bestScore = 0;
|
|
74
|
+
for (const g of GROUPS) {
|
|
75
|
+
let score = 0;
|
|
76
|
+
for (const w of g.words) if (text.includes(w)) score += w.includes(' ') || w.includes('-') ? 3 : 1;
|
|
77
|
+
if (score > bestScore) {
|
|
78
|
+
bestScore = score;
|
|
79
|
+
best = g;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return best ?? { key: 'other', label: 'Other' };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* ------------------------------------------------------------------- roots */
|
|
86
|
+
|
|
87
|
+
function rootsFromArgs(argv) {
|
|
88
|
+
const roots = [];
|
|
89
|
+
for (let i = 0; i < argv.length; i++) {
|
|
90
|
+
if (argv[i] === '--root' && argv[i + 1]) roots.push(resolve(argv[i + 1]));
|
|
91
|
+
}
|
|
92
|
+
if (roots.length) return roots;
|
|
93
|
+
|
|
94
|
+
const env = process.env.SKILL_ROOTS;
|
|
95
|
+
if (env) {
|
|
96
|
+
const fromEnv = env.split(delimiter).map((p) => p.trim()).filter(Boolean).map(resolve);
|
|
97
|
+
if (fromEnv.length) return fromEnv;
|
|
98
|
+
}
|
|
99
|
+
return DEFAULT_ROOTS;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function findSkillFiles(root) {
|
|
103
|
+
const found = [];
|
|
104
|
+
const stack = [{ path: root, depth: 0 }];
|
|
105
|
+
while (stack.length) {
|
|
106
|
+
const { path, depth } = stack.pop();
|
|
107
|
+
if (depth > MAX_DEPTH) continue;
|
|
108
|
+
let entries;
|
|
109
|
+
try {
|
|
110
|
+
entries = readdirSync(path, { withFileTypes: true });
|
|
111
|
+
} catch {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
for (const e of entries) {
|
|
115
|
+
const full = join(path, e.name);
|
|
116
|
+
if (!e.isDirectory()) {
|
|
117
|
+
if (e.name === 'SKILL.md') found.push(full);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (SKIP_DIRS.has(e.name) || e.name.startsWith('.')) continue;
|
|
121
|
+
try {
|
|
122
|
+
if (statSync(full).isDirectory()) stack.push({ path: full, depth: depth + 1 });
|
|
123
|
+
} catch {
|
|
124
|
+
/* unreadable */
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return found;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* -------------------------------------------------------------- frontmatter */
|
|
132
|
+
|
|
133
|
+
function parseFrontmatter(text) {
|
|
134
|
+
const lines = text.split(/\r?\n/);
|
|
135
|
+
if (lines[0]?.trim() !== '---') return {};
|
|
136
|
+
const end = lines.findIndex((l, i) => i > 0 && l.trim() === '---');
|
|
137
|
+
if (end < 0) return {};
|
|
138
|
+
|
|
139
|
+
const fields = {};
|
|
140
|
+
for (let i = 1; i < end; i++) {
|
|
141
|
+
const m = lines[i].match(/^([A-Za-z_-]+):\s*(.*)$/);
|
|
142
|
+
if (!m) continue;
|
|
143
|
+
const [, key, raw] = m;
|
|
144
|
+
const value = raw.trim();
|
|
145
|
+
if (['', '|', '>', '|-', '>-', '|+', '>+'].includes(value)) {
|
|
146
|
+
const cont = [];
|
|
147
|
+
while (i + 1 < end && /^\s+\S/.test(lines[i + 1])) cont.push(lines[++i].trim());
|
|
148
|
+
fields[key] = cont.join(' ').trim();
|
|
149
|
+
} else {
|
|
150
|
+
fields[key] = value.replace(/^["']|["']$/g, '').trim();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return fields;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* --------------------------------------------------------------- copy trees */
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Translated documentation trees are the most common source of phantom
|
|
160
|
+
* skills: a repo ships docs/<locale>/skills alongside the real ones and every
|
|
161
|
+
* locale is discovered as a separate capability. They add no reach.
|
|
162
|
+
*/
|
|
163
|
+
const LOCALE_SEGMENT = /^[a-z]{2}(?:[-_][A-Za-z]{2,4})?$/;
|
|
164
|
+
|
|
165
|
+
function isLocaleCopy(path) {
|
|
166
|
+
const parts = path.split(/[\\/]/);
|
|
167
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
168
|
+
if ((parts[i] === 'docs' || parts[i] === 'doc') && LOCALE_SEGMENT.test(parts[i + 1])) return true;
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* -------------------------------------------------------------------- scan */
|
|
174
|
+
|
|
175
|
+
function scan(roots) {
|
|
176
|
+
const records = [];
|
|
177
|
+
const seen = new Set();
|
|
178
|
+
|
|
179
|
+
for (const root of roots) {
|
|
180
|
+
if (!existsSync(root)) continue;
|
|
181
|
+
for (const file of findSkillFiles(root)) {
|
|
182
|
+
if (seen.has(file)) continue;
|
|
183
|
+
seen.add(file);
|
|
184
|
+
let fm = {};
|
|
185
|
+
let text = '';
|
|
186
|
+
try {
|
|
187
|
+
text = readFileSync(file, 'utf8');
|
|
188
|
+
fm = parseFrontmatter(text);
|
|
189
|
+
} catch {
|
|
190
|
+
/* unreadable */
|
|
191
|
+
}
|
|
192
|
+
const name = fm.name || basename(dirname(file));
|
|
193
|
+
const description = (fm.description || '').replace(/\s+/g, ' ').trim();
|
|
194
|
+
const group = classify(name, description);
|
|
195
|
+
const localeCopy = isLocaleCopy(file);
|
|
196
|
+
records.push({
|
|
197
|
+
name,
|
|
198
|
+
description,
|
|
199
|
+
group: group.key,
|
|
200
|
+
groupLabel: group.label,
|
|
201
|
+
path: file,
|
|
202
|
+
root,
|
|
203
|
+
copy: localeCopy,
|
|
204
|
+
origin: localeCopy ? 'locale' : null,
|
|
205
|
+
hash: createHash('sha1').update(text).digest('hex'),
|
|
206
|
+
disabled: String(fm['disable-model-invocation'] ?? '').toLowerCase() === 'true',
|
|
207
|
+
weak: description.length < WEAK_DESCRIPTION,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Second detector: byte-identical files. Repos often ship a mirrored tree
|
|
213
|
+
// (a second copy of skills/ under another folder) that is not a locale copy
|
|
214
|
+
// and therefore escapes the path rule.
|
|
215
|
+
//
|
|
216
|
+
// Canonical selection matters: a file under a real `skills/` directory always
|
|
217
|
+
// beats a mirror, otherwise a folder named "mirror" can win on path length and
|
|
218
|
+
// the reported path sends the reader to the copy.
|
|
219
|
+
const canonicalScore = (r) => [/[\\/]skills[\\/]/i.test(r.path) ? 0 : 1, r.path.length];
|
|
220
|
+
const canonical = (a, b) => {
|
|
221
|
+
const [ax, ay] = canonicalScore(a);
|
|
222
|
+
const [bx, by] = canonicalScore(b);
|
|
223
|
+
return ax - bx || ay - by || a.path.localeCompare(b.path);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const byContent = new Map();
|
|
227
|
+
for (const r of records) {
|
|
228
|
+
if (r.copy) continue;
|
|
229
|
+
const key = `${r.name.toLowerCase()}|${r.hash}`;
|
|
230
|
+
const list = byContent.get(key) ?? [];
|
|
231
|
+
list.push(r);
|
|
232
|
+
byContent.set(key, list);
|
|
233
|
+
}
|
|
234
|
+
for (const list of byContent.values()) {
|
|
235
|
+
if (list.length < 2) continue;
|
|
236
|
+
list.sort(canonical);
|
|
237
|
+
for (const r of list.slice(1)) {
|
|
238
|
+
r.copy = true;
|
|
239
|
+
r.origin = 'duplicate';
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
for (const r of records) delete r.hash;
|
|
244
|
+
records.sort((a, b) => a.name.localeCompare(b.name));
|
|
245
|
+
return records;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/* ----------------------------------------------------------------- reports */
|
|
249
|
+
|
|
250
|
+
function stat(records) {
|
|
251
|
+
const genuine = records.filter((r) => !r.copy);
|
|
252
|
+
const copies = records.length - genuine.length;
|
|
253
|
+
const byGroup = new Map();
|
|
254
|
+
for (const r of genuine) byGroup.set(r.groupLabel, (byGroup.get(r.groupLabel) || 0) + 1);
|
|
255
|
+
|
|
256
|
+
const byName = new Map();
|
|
257
|
+
for (const r of genuine) {
|
|
258
|
+
const list = byName.get(r.name.toLowerCase()) || [];
|
|
259
|
+
list.push(r);
|
|
260
|
+
byName.set(r.name.toLowerCase(), list);
|
|
261
|
+
}
|
|
262
|
+
const dupes = [...byName.values()].filter((l) => l.length > 1);
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
total: records.length,
|
|
266
|
+
genuine: genuine.length,
|
|
267
|
+
copies,
|
|
268
|
+
localeCopies: records.filter((r) => r.origin === 'locale').length,
|
|
269
|
+
duplicateCopies: records.filter((r) => r.origin === 'duplicate').length,
|
|
270
|
+
uniqueNames: new Set(genuine.map((r) => r.name.toLowerCase())).size,
|
|
271
|
+
byGroup,
|
|
272
|
+
dupes,
|
|
273
|
+
weak: genuine.filter((r) => r.weak),
|
|
274
|
+
disabled: genuine.filter((r) => r.disabled).length,
|
|
275
|
+
roots: [...new Set(records.map((r) => r.root))],
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function summary(records) {
|
|
280
|
+
const s = stat(records);
|
|
281
|
+
const out = [];
|
|
282
|
+
out.push('SKILL CATALOGUE');
|
|
283
|
+
out.push('='.repeat(64));
|
|
284
|
+
out.push(`Roots with skills : ${s.roots.length}`);
|
|
285
|
+
for (const root of s.roots) {
|
|
286
|
+
const all = records.filter((r) => r.root === root).length;
|
|
287
|
+
const real = records.filter((r) => r.root === root && !r.copy).length;
|
|
288
|
+
out.push(` ${root}`);
|
|
289
|
+
out.push(` ${all} files, ${real} genuine`);
|
|
290
|
+
}
|
|
291
|
+
out.push('');
|
|
292
|
+
out.push(`Total SKILL.md files : ${s.total}`);
|
|
293
|
+
out.push(` genuine : ${s.genuine}`);
|
|
294
|
+
out.push(
|
|
295
|
+
` copies : ${s.copies}${s.total ? ` (${Math.round((s.copies / s.total) * 100)}%)` : ''}` +
|
|
296
|
+
` [locale ${s.localeCopies}, identical ${s.duplicateCopies}]`,
|
|
297
|
+
);
|
|
298
|
+
out.push(` unique names (genuine) : ${s.uniqueNames}`);
|
|
299
|
+
out.push(` duplicate names : ${s.dupes.length}`);
|
|
300
|
+
out.push(` weak descriptions : ${s.weak.length}`);
|
|
301
|
+
out.push('');
|
|
302
|
+
out.push('By group (genuine only):');
|
|
303
|
+
for (const [label, n] of [...s.byGroup.entries()].sort((a, b) => b[1] - a[1])) {
|
|
304
|
+
out.push(` ${label.padEnd(38)} ${String(n).padStart(4)}`);
|
|
305
|
+
}
|
|
306
|
+
return out.join('\n');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function search(records, query, groupFilter) {
|
|
310
|
+
const q = query.toLowerCase().trim();
|
|
311
|
+
if (!q) return 'Error: --search needs a query.';
|
|
312
|
+
const pool = records.filter(
|
|
313
|
+
(r) => !r.copy && (!groupFilter || r.group === groupFilter || r.groupLabel.toLowerCase().includes(groupFilter)),
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
317
|
+
// Plural tolerance so "captions" still finds "caption".
|
|
318
|
+
const forms = new Set([q, q.endsWith('s') ? q.slice(0, -1) : `${q}s`]);
|
|
319
|
+
const alts = [...forms].map(escape).join('|');
|
|
320
|
+
// A match must sit on real word boundaries: "expo" must not match "exposed".
|
|
321
|
+
const wordRe = new RegExp(`(^|[^a-z0-9])(${alts})(?![a-z0-9])`);
|
|
322
|
+
|
|
323
|
+
const scored = pool
|
|
324
|
+
.map((r) => {
|
|
325
|
+
const name = r.name.toLowerCase();
|
|
326
|
+
const segments = name.split(/[^a-z0-9+#.]+/).filter(Boolean);
|
|
327
|
+
let score = 0;
|
|
328
|
+
if (name === q) score += 100;
|
|
329
|
+
if (segments.some((s) => forms.has(s))) score += 60;
|
|
330
|
+
else if (name.includes(q)) score += 15; // weak: tie-breaker only
|
|
331
|
+
if (wordRe.test(name)) score += 20;
|
|
332
|
+
if (wordRe.test(r.description.toLowerCase())) score += 25;
|
|
333
|
+
return { r, score };
|
|
334
|
+
})
|
|
335
|
+
.filter((x) => x.score > 0)
|
|
336
|
+
.sort((a, b) => b.score - a.score || a.r.name.localeCompare(b.r.name));
|
|
337
|
+
|
|
338
|
+
if (!scored.length) return `No skill matches "${query}".`;
|
|
339
|
+
const out = [`"${query}" -> ${scored.length} match(es)`, ''];
|
|
340
|
+
for (const { r } of scored.slice(0, 25)) {
|
|
341
|
+
out.push(`* ${r.name} [${r.groupLabel}]`);
|
|
342
|
+
if (r.description) out.push(` ${r.description.slice(0, 220)}${r.description.length > 220 ? '...' : ''}`);
|
|
343
|
+
out.push(` ${r.path}`);
|
|
344
|
+
}
|
|
345
|
+
return out.join('\n');
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function groupList(records) {
|
|
349
|
+
const s = stat(records);
|
|
350
|
+
const rows = [...s.byGroup.entries()].sort((a, b) => b[1] - a[1]);
|
|
351
|
+
const out = rows.map(([label, n]) => {
|
|
352
|
+
const key = GROUPS.find((g) => g.label === label)?.key ?? 'other';
|
|
353
|
+
return `${key.padEnd(12)} ${label.padEnd(38)} ${n}`;
|
|
354
|
+
});
|
|
355
|
+
if (rows.length) out.unshift('key label count');
|
|
356
|
+
return out.join('\n');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function dupes(records) {
|
|
360
|
+
const s = stat(records);
|
|
361
|
+
if (!s.dupes.length) return 'No duplicate names.';
|
|
362
|
+
const out = [`${s.dupes.length} duplicate name(s):`];
|
|
363
|
+
for (const list of s.dupes) {
|
|
364
|
+
out.push('');
|
|
365
|
+
out.push(`${list[0].name} (${list.length})`);
|
|
366
|
+
for (const r of list) out.push(` ${r.path}`);
|
|
367
|
+
}
|
|
368
|
+
return out.join('\n');
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function weak(records) {
|
|
372
|
+
const s = stat(records);
|
|
373
|
+
if (!s.weak.length) return 'No weak descriptions.';
|
|
374
|
+
return s.weak.map((r) => `${r.name} (${r.description.length} chars) ${r.path}`).join('\n');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/* --------------------------------------------------------------------- cli */
|
|
378
|
+
|
|
379
|
+
const argv = process.argv.slice(2);
|
|
380
|
+
const valueOf = (flag) => {
|
|
381
|
+
const i = argv.indexOf(flag);
|
|
382
|
+
return i >= 0 ? argv[i + 1] : undefined;
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
386
|
+
console.log(readFileSync(new URL(import.meta.url), 'utf8').split('*/')[0].replace(/^#!.*\n/, ''));
|
|
387
|
+
process.exit(0);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const roots = rootsFromArgs(argv);
|
|
391
|
+
const records = scan(roots);
|
|
392
|
+
|
|
393
|
+
if (!roots.some((r) => existsSync(r))) {
|
|
394
|
+
console.error('No skill root found. Pass --root <path> or set SKILL_ROOTS.');
|
|
395
|
+
process.exit(1);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (argv.includes('--json')) {
|
|
399
|
+
console.log(JSON.stringify(records, null, 2));
|
|
400
|
+
} else if (argv.includes('--search') || argv.includes('--grep')) {
|
|
401
|
+
console.log(search(records, valueOf('--search') ?? valueOf('--grep') ?? '', valueOf('--group')));
|
|
402
|
+
} else if (argv.includes('--groups')) {
|
|
403
|
+
console.log(groupList(records));
|
|
404
|
+
} else if (argv.includes('--dupes')) {
|
|
405
|
+
console.log(dupes(records));
|
|
406
|
+
} else if (argv.includes('--weak')) {
|
|
407
|
+
console.log(weak(records));
|
|
408
|
+
} else {
|
|
409
|
+
console.log(summary(records));
|
|
410
|
+
const outDir = valueOf('--write');
|
|
411
|
+
if (outDir) {
|
|
412
|
+
try {
|
|
413
|
+
mkdirSync(resolve(outDir), { recursive: true });
|
|
414
|
+
writeFileSync(join(resolve(outDir), 'skills.json'), JSON.stringify(records, null, 2), 'utf8');
|
|
415
|
+
console.log(`\nWrote ${join(resolve(outDir), 'skills.json')}`);
|
|
416
|
+
} catch (e) {
|
|
417
|
+
console.error(`Could not write index: ${e.message}`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cangurel81/skill-router",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Index, classify and search a large agent-skill catalogue. Answers \"which skill fits this job?\" and reports catalogue bloat caused by duplicated locale trees.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"skill-router": "bin/skill-router.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"skills/",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"keywords": [
|
|
16
|
+
"agent",
|
|
17
|
+
"skills",
|
|
18
|
+
"skill-router",
|
|
19
|
+
"claude-code",
|
|
20
|
+
"opencode",
|
|
21
|
+
"codex",
|
|
22
|
+
"ai-agents",
|
|
23
|
+
"tooling",
|
|
24
|
+
"cli"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"author": "Can Gürel (https://webadhere.com)",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/cangurel81/skill-router.git"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/cangurel81/skill-router#readme",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/cangurel81/skill-router/issues"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"start": "node bin/skill-router.mjs",
|
|
44
|
+
"test": "node --check bin/skill-router.mjs && node test/smoke.mjs"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: skill-router
|
|
3
|
+
description: Use when a task may already be covered by one of the hundreds of installed agent skills and you need the right one instead of guessing — "which skill for this", "do we already have a skill for X", "is there a skill that does Y", "my skill folder is a mess", "too many skills", or when two installed skills overlap and one must be picked. Scans every configured skill root, classifies skills by domain, separates genuine skills from translated locale copies, and returns ranked matches with file paths plus a measured bloat report. Do not use for authoring a new skill (use writing-skills) or for installing a repository of skills.
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Skill Router
|
|
8
|
+
|
|
9
|
+
A large skill catalogue fails in two directions. The agent picks a skill that does not exist,
|
|
10
|
+
or it writes from scratch something already installed forty times over. Both come from guessing
|
|
11
|
+
instead of looking.
|
|
12
|
+
|
|
13
|
+
**Rule: never claim "no skill covers this" and never name a skill without running the router.**
|
|
14
|
+
|
|
15
|
+
## Run it
|
|
16
|
+
|
|
17
|
+
From the repository that contains this skill:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
node bin/skill-router.mjs # full catalogue inventory
|
|
21
|
+
node bin/skill-router.mjs --search expo # ranked matches with paths
|
|
22
|
+
node bin/skill-router.mjs --search video --group video
|
|
23
|
+
node bin/skill-router.mjs --groups # domain keys and counts
|
|
24
|
+
node bin/skill-router.mjs --dupes # same name, different folders
|
|
25
|
+
node bin/skill-router.mjs --weak # descriptions too short to route on
|
|
26
|
+
node bin/skill-router.mjs --json # machine-readable
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Point it somewhere explicit:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
node bin/skill-router.mjs --root ~/.claude/skills --search testing
|
|
33
|
+
node bin/skill-router.mjs --root ./vendor/skills --root ./skills
|
|
34
|
+
SKILL_ROOTS=~/.claude/skills:./skills node bin/skill-router.mjs
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`--root` (repeatable) beats `SKILL_ROOTS`, which beats the built-in defaults
|
|
38
|
+
(`./.claude/skills`, `./.agents/skills`, `./skills`, `~/.claude/skills`, `~/.config/opencode/skills`,
|
|
39
|
+
`~/.codex/skills`, `~/.kimi-code/skills`).
|
|
40
|
+
|
|
41
|
+
Read the **path** in every result before trusting the name. Two skills can share a name and differ
|
|
42
|
+
completely; the path is the only thing that says which repo you are actually loading.
|
|
43
|
+
|
|
44
|
+
## Routing procedure
|
|
45
|
+
|
|
46
|
+
1. **Name the domain first.** `--groups` gives the keys. Searching inside a domain beats searching
|
|
47
|
+
the whole catalogue, because generic words like "review", "audit" or "data" match hundreds.
|
|
48
|
+
2. **Search two or three different words**, not one. A skill whose name says `tts` may only
|
|
49
|
+
mention "voiceover" in its description; the ranker searches both.
|
|
50
|
+
3. **Read the description, then the path.** Description tells you if it fits. Path tells you which
|
|
51
|
+
repo and whether it is a locale copy.
|
|
52
|
+
4. **Only then open the SKILL.md.** Opening every candidate is what the index exists to avoid.
|
|
53
|
+
5. **If two match and overlap**, pick one, and check `--dupes` to see whether your catalogue carries
|
|
54
|
+
both permanently.
|
|
55
|
+
|
|
56
|
+
## Domain keys
|
|
57
|
+
|
|
58
|
+
| Key | Domain | Example search terms |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| `seo` | SEO / GEO / search | `geo`, `citability`, `backlink`, `schema`, `llms.txt` |
|
|
61
|
+
| `video` | Video / motion / audio | `remotion`, `ffmpeg`, `caption`, `tts`, `shorts` |
|
|
62
|
+
| `frontend` | Frontend / UI / design system | `react`, `vue`, `tailwind`, `gsap`, `a11y` |
|
|
63
|
+
| `backend` | Backend / API / databases | `laravel`, `fastapi`, `postgres`, `graphql` |
|
|
64
|
+
| `mobile` | Mobile / native app work | `expo`, `flutter`, `swift`, `app store` |
|
|
65
|
+
| `marketing` | Marketing / content / social | `linkedin`, `carousel`, `hook`, `campaign` |
|
|
66
|
+
| `agent` | Agent / LLM / orchestration | `harness`, `subagent`, `eval`, `memory` |
|
|
67
|
+
| `security` | Security / compliance | `audit`, `hardening`, `threat model` |
|
|
68
|
+
| `infra` | Infra / DevOps / network | `docker`, `ci/cd`, `dns`, `vpn` |
|
|
69
|
+
| `data` | Data / research / analytics | `scrape`, `sql`, `dashboard`, `benchmark` |
|
|
70
|
+
| `code` | Code quality / testing | `tdd`, `review`, `refactor`, `lint` |
|
|
71
|
+
| `desktop` | Desktop / documents | `pdf`, `pandoc`, `screenshot` |
|
|
72
|
+
| `wordpress` | WordPress / WooCommerce | `plugin`, `woocommerce`, `wp-cli` |
|
|
73
|
+
| `other` | Unclassified | fix the description |
|
|
74
|
+
|
|
75
|
+
`other` being large is a finding, not a category. Skills land there because their description is
|
|
76
|
+
too vague to classify, which also means no agent can route to them.
|
|
77
|
+
|
|
78
|
+
## Reading the inventory
|
|
79
|
+
|
|
80
|
+
The summary reports five numbers. Each one has an action attached:
|
|
81
|
+
|
|
82
|
+
| Number | Means | Action |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| `locale doc copies` above ~10% | repos are registered at their root, so `docs/<locale>/skills` are scanned as skills | register the real `skills/` folder instead |
|
|
85
|
+
| `duplicate names` high | two repos ship the same skill; the agent may load either | pick one and drop the other root, or accept the newer repo |
|
|
86
|
+
| `weak descriptions` | description under 25 chars, unusable for routing | extend the description or treat the skill as absent |
|
|
87
|
+
| `genuine` far below `total` | most of the catalogue is copies | fix roots before adding anything |
|
|
88
|
+
| one domain with 0 | real capability gap | this is where a new skill is justified |
|
|
89
|
+
|
|
90
|
+
The tool never deletes anything and never edits a skill. It measures and reports.
|
|
91
|
+
|
|
92
|
+
## When a new skill is actually justified
|
|
93
|
+
|
|
94
|
+
All three must hold:
|
|
95
|
+
|
|
96
|
+
1. `--search` over three plausible terms returns nothing relevant.
|
|
97
|
+
2. The gap is knowledge no public repository could ship — local toolchain, local traps, local
|
|
98
|
+
decisions, private systems.
|
|
99
|
+
3. The knowledge repeats. It has already cost time twice.
|
|
100
|
+
|
|
101
|
+
Otherwise extend an existing skill or fix its description. A fourth near-duplicate skill makes the
|
|
102
|
+
catalogue worse, not more capable.
|
|
103
|
+
|
|
104
|
+
## What this does not do
|
|
105
|
+
|
|
106
|
+
- It does not install, fetch or update skills.
|
|
107
|
+
- It does not validate that a `SKILL.md` is correct — only that it is discoverable.
|
|
108
|
+
- It does not judge quality. A short description is a routing failure, not necessarily a bad skill.
|
|
109
|
+
- It does not read the skill body. Content review is a separate pass.
|