skills-atlas-cli 0.7.0 โ 0.8.2
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/README.md +68 -60
- package/bin/skills.js +5 -1
- package/package.json +1 -1
- package/src/args.js +1 -0
- package/src/commands/categories.js +7 -1
- package/src/commands/doctor.js +2 -2
- package/src/commands/hook.js +14 -5
- package/src/commands/info.js +2 -1
- package/src/commands/install.js +31 -17
- package/src/commands/mcp.js +5 -0
- package/src/commands/remove.js +8 -0
- package/src/commands/search.js +13 -6
- package/src/commands/update.js +1 -0
- package/src/commands/upgrade.js +9 -2
- package/src/data.js +22 -5
- package/src/format.js +7 -2
- package/src/mcp.js +190 -0
- package/src/search-core.js +17 -2
package/README.md
CHANGED
|
@@ -3,57 +3,43 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/skills-atlas-cli)
|
|
4
4
|
[](https://github.com/Zita-Go/Skills-Atlas/blob/main/LICENSE)
|
|
5
5
|
|
|
6
|
-
**
|
|
7
|
-
|
|
6
|
+
**Find, install, and use the right AI agent skill for any task, right inside Claude
|
|
7
|
+
Code.** Stop guessing which skill fits or copy-pasting from random repos. Search a
|
|
8
|
+
curated catalog of **800+ skills** and drop the right one into `.claude/skills/` in seconds.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
> **New to "skills"?** A skill is a reusable `SKILL.md` instruction pack that teaches
|
|
11
|
+
> Claude Code a specialized workflow: systematic debugging, pre-mortems, SEO audits,
|
|
12
|
+
> PDF translation, and hundreds more. This tool finds and installs them for you.
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
## ๐ Browse the whole catalog online
|
|
14
|
+
### Two ways to get the right skill
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
<td><a href="https://zita-go.github.io/Skills-Atlas/"><img src="https://raw.githubusercontent.com/Zita-Go/Skills-Atlas/main/docs/screenshot-light.png" alt="Skills Atlas โ light theme" width="400"></a></td>
|
|
20
|
-
<td><a href="https://zita-go.github.io/Skills-Atlas/"><img src="https://raw.githubusercontent.com/Zita-Go/Skills-Atlas/main/docs/screenshot-dark.png" alt="Skills Atlas โ dark theme" width="400"></a></td>
|
|
21
|
-
</tr>
|
|
22
|
-
</table>
|
|
16
|
+
1. **๐ Find and install it.** Search the catalog, then `use` it. It's live in Claude Code in seconds.
|
|
17
|
+
2. **๐ค Let it find you.** Turn on autopilot and Claude offers the fitting skill as you work, no searching.
|
|
23
18
|
|
|
24
|
-
|
|
25
|
-
explore by category, then install what you find with the CLI.
|
|
19
|
+
<img src="https://raw.githubusercontent.com/Zita-Go/Skills-Atlas/main/docs/cli-demo.png" alt="skills-atlas: search then install a skill" width="760">
|
|
26
20
|
|
|
27
|
-
##
|
|
21
|
+
## Quickstart
|
|
28
22
|
|
|
29
23
|
```bash
|
|
30
|
-
npm install -g skills-atlas-cli # adds the `skills-atlas` command (alias
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
Or run it without installing: `npx skills-atlas-cli search seo`
|
|
24
|
+
npm install -g skills-atlas-cli # adds the `skills-atlas` command (alias sa). Or run any command with `npx`.
|
|
34
25
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
26
|
+
skills-atlas search "stress test my launch plan" # pre-mortem tops the results
|
|
27
|
+
skills-atlas use pre-mortem # installs it, prints its SKILL.md so Claude applies it now
|
|
28
|
+
skills-atlas hook on # optional: turn on autopilot, skip the search next time
|
|
29
|
+
```
|
|
38
30
|
|
|
39
|
-
|
|
40
|
-
|---|---|---|
|
|
41
|
-
| **Manual** | you want to browse and grab skills yourself | `skills-atlas search <task>` โ `skills-atlas use <skill>` |
|
|
42
|
-
| **In Claude Code** | you'd rather just ask Claude in-conversation | install the [plugin](#in-claude-code), then describe your task |
|
|
43
|
-
| **๐ค Autopilot** | you want the right skill to find *you* | `skills-atlas hook on` โ Claude offers a fitting skill as you work |
|
|
31
|
+
Now `pre-mortem` lives in `~/.claude/skills/` and auto-loads in new Claude Code sessions.
|
|
44
32
|
|
|
45
|
-
|
|
33
|
+
## ๐ Browse the catalog online
|
|
46
34
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
35
|
+
<table>
|
|
36
|
+
<tr>
|
|
37
|
+
<td><a href="https://zita-go.github.io/Skills-Atlas/"><img src="https://raw.githubusercontent.com/Zita-Go/Skills-Atlas/main/docs/screenshot-light.png" alt="Skills Atlas โ light theme" width="400"></a></td>
|
|
38
|
+
<td><a href="https://zita-go.github.io/Skills-Atlas/"><img src="https://raw.githubusercontent.com/Zita-Go/Skills-Atlas/main/docs/screenshot-dark.png" alt="Skills Atlas โ dark theme" width="400"></a></td>
|
|
39
|
+
</tr>
|
|
40
|
+
</table>
|
|
53
41
|
|
|
54
|
-
|
|
55
|
-
auto-loads in new Claude Code sessions. With autopilot on, you don't even need to
|
|
56
|
-
`search` โ describe your task and Claude surfaces the skill if one fits.
|
|
42
|
+
Explore the catalog visually at **[zita-go.github.io/Skills-Atlas](https://zita-go.github.io/Skills-Atlas/)**, then install what you find with the CLI.
|
|
57
43
|
|
|
58
44
|
## Usage
|
|
59
45
|
|
|
@@ -70,7 +56,7 @@ skills-atlas info brainstorming
|
|
|
70
56
|
skills-atlas install brainstorming # โ ~/.claude/skills/ (default, all projects)
|
|
71
57
|
skills-atlas install brainstorming --project # โ ./.claude/skills/ (this project only)
|
|
72
58
|
skills-atlas install brainstorming --chain # install the whole โ workflow it belongs to
|
|
73
|
-
skills-atlas use brainstorming # install
|
|
59
|
+
skills-atlas use brainstorming # install + activate now (prints SKILL.md, no restart)
|
|
74
60
|
skills-atlas install brainstorming --dry-run # preview the files, write nothing
|
|
75
61
|
|
|
76
62
|
# ๐๏ธ Manage what you've installed (like a package manager)
|
|
@@ -84,10 +70,18 @@ skills-atlas doctor # health check: orphans, drift, l
|
|
|
84
70
|
skills-atlas kit # detect this project & install the right skills for it
|
|
85
71
|
skills-atlas sync # reproduce a project's kit (skills-atlas.kit.json)
|
|
86
72
|
|
|
87
|
-
#
|
|
73
|
+
# ๐ค Autopilot & capability gaps (opt-in)
|
|
74
|
+
skills-atlas hook on # Claude proactively offers a fitting skill as you work
|
|
75
|
+
skills-atlas gaps # Claude spots kinds of work you keep doing without a skill
|
|
76
|
+
|
|
77
|
+
# ๐ Catalog & sources
|
|
88
78
|
skills-atlas categories # the 20 top-level categories
|
|
89
79
|
skills-atlas list marketing # skill groups within a category
|
|
80
|
+
skills-atlas registry add <url|path> # add a private org catalog source (merges into search/install)
|
|
90
81
|
skills-atlas update # pull the latest catalog
|
|
82
|
+
|
|
83
|
+
# ๐ Integrations
|
|
84
|
+
skills-atlas mcp # run as an MCP server (any MCP client: Claude Desktop, โฆ)
|
|
91
85
|
```
|
|
92
86
|
|
|
93
87
|
**โ Workflows, not just skills.** Many skills belong to a curated chain (e.g.
|
|
@@ -95,8 +89,8 @@ skills-atlas update # pull the latest catalog
|
|
|
95
89
|
installs the whole pipeline in one archive download, ready to run in order.
|
|
96
90
|
|
|
97
91
|
**๐ฆ Project kits.** `skills-atlas kit` detects what this project is (frontend / backend /
|
|
98
|
-
data / infra) and installs a tailored set
|
|
99
|
-
add-ons
|
|
92
|
+
data / infra) and installs a tailored set (a universal dev workflow plus archetype
|
|
93
|
+
add-ons) into `./.claude/skills/`, then writes a committable `skills-atlas.kit.json`.
|
|
100
94
|
A teammate runs `skills-atlas sync` to reproduce it exactly.
|
|
101
95
|
|
|
102
96
|
Output is English by default; add `--zh` for Chinese, or `--json` to any command for machine-readable output.
|
|
@@ -105,16 +99,16 @@ After installing a skill, start a new Claude Code session to load it.
|
|
|
105
99
|
## How install works
|
|
106
100
|
|
|
107
101
|
The real value is the **catalog**: `search` / `info` / `categories` work fully
|
|
108
|
-
offline and map *which* skill fits
|
|
102
|
+
offline and map *which* skill fits. It's function-organized, bilingual, tagged with
|
|
109
103
|
use-case / when-to-use / personas / โ chains. That's what `npx skills add` and
|
|
110
104
|
GitHub search don't give you.
|
|
111
105
|
|
|
112
106
|
On top of that, `install` can place a skill straight into `.claude/skills/`:
|
|
113
107
|
|
|
114
108
|
- For a repo that exposes a **per-skill folder**, it downloads only that folder
|
|
115
|
-
(via the repo archive
|
|
109
|
+
(via the repo archive, with **no GitHub API rate limit**) into
|
|
116
110
|
`<target>/.claude/skills/<skill>/`, not the whole repo.
|
|
117
|
-
- Several sources? The best installable one is auto-picked
|
|
111
|
+
- Several sources? The best installable one is auto-picked. Pass `--source <id>` to
|
|
118
112
|
choose, `--yes` for non-interactive runs.
|
|
119
113
|
- Other sources (whole-repo / marketplace) print their official command instead
|
|
120
114
|
(e.g. `npx skills add owner/repo`).
|
|
@@ -127,8 +121,8 @@ the latest from the public feed (cached under `~/.cache/skills-atlas/`).
|
|
|
127
121
|
|
|
128
122
|
## Private / org catalog sources
|
|
129
123
|
|
|
130
|
-
Point the CLI at your organization's own catalog
|
|
131
|
-
schema
|
|
124
|
+
Point the CLI at your organization's own catalog (a `data.json` in the same
|
|
125
|
+
schema) so internal skills show up in `search` / `info` / `install` / `kit`
|
|
132
126
|
alongside the public Atlas:
|
|
133
127
|
|
|
134
128
|
```bash
|
|
@@ -144,8 +138,8 @@ auth, set `SKILLS_ATLAS_TOKEN` (sent as a Bearer header); in CI,
|
|
|
144
138
|
|
|
145
139
|
## In Claude Code
|
|
146
140
|
|
|
147
|
-
A thin [Claude Code plugin](./plugin) lets Claude do all of this in-conversation
|
|
148
|
-
|
|
141
|
+
A thin [Claude Code plugin](./plugin) lets Claude do all of this in-conversation.
|
|
142
|
+
Just describe what you need, or use `/skills-atlas:skill-search`, `:skill-info`,
|
|
149
143
|
`:skill-install`:
|
|
150
144
|
|
|
151
145
|
```text
|
|
@@ -153,7 +147,21 @@ just describe what you need, or use `/skills-atlas:skill-search`, `:skill-info`,
|
|
|
153
147
|
/plugin install skills-atlas@skills-atlas
|
|
154
148
|
```
|
|
155
149
|
|
|
156
|
-
##
|
|
150
|
+
## In any MCP client
|
|
151
|
+
|
|
152
|
+
`skills-atlas mcp` runs a zero-dependency [MCP](https://modelcontextprotocol.io)
|
|
153
|
+
server over stdio, so any MCP-capable client (Claude Desktop, other agents) can use
|
|
154
|
+
the catalog. Add it to your client's config:
|
|
155
|
+
|
|
156
|
+
```json
|
|
157
|
+
{ "mcpServers": { "skills-atlas": { "command": "npx", "args": ["-y", "skills-atlas-cli", "mcp"] } } }
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
It exposes four tools: **search_skills**, **skill_info**, **install_skill**, and
|
|
161
|
+
**list_categories**. Discover, inspect, install, and browse the catalog from
|
|
162
|
+
anywhere.
|
|
163
|
+
|
|
164
|
+
## Autopilot (opt-in): the right skill finds you
|
|
157
165
|
|
|
158
166
|
```bash
|
|
159
167
|
skills-atlas hook on # enable (skills-atlas hook off / status)
|
|
@@ -161,29 +169,29 @@ skills-atlas hook on # enable (skills-atlas hook off / status)
|
|
|
161
169
|
|
|
162
170
|
Registers a Claude Code `UserPromptSubmit` hook. When what you ask matches the
|
|
163
171
|
territory of a catalog skill you don't have, the hook hands Claude a short
|
|
164
|
-
shortlist of candidates and **Claude decides** whether any genuinely fits
|
|
165
|
-
|
|
172
|
+
shortlist of candidates and **Claude decides** whether any genuinely fits. If it
|
|
173
|
+
does, Claude explains **what it does and why it fits your task**, then offers a choice:
|
|
166
174
|
use it now, see what it covers first (`skills-atlas info`), or skip. You don't
|
|
167
175
|
have to know the skill exists. The split is deliberate: the hook does **recall**
|
|
168
176
|
(a distinctive-word match against the catalog, so the right skill is on the
|
|
169
177
|
table), Claude does **precision** (it understands your intent and stays silent
|
|
170
178
|
unless one truly fits, or searches further itself). It's:
|
|
171
179
|
|
|
172
|
-
- **
|
|
173
|
-
- **
|
|
180
|
+
- **Off by default.** You turn it on explicitly; `hook off` removes it cleanly.
|
|
181
|
+
- **Quiet.** Only fires on a distinctive match (greetings and generic actions
|
|
174
182
|
like "fix the typo" stay silent), never for an already-installed skill, never
|
|
175
|
-
the same skill twice, with a cooldown between suggestions
|
|
183
|
+
the same skill twice, with a cooldown between suggestions. Claude is the
|
|
176
184
|
final filter on relevance.
|
|
177
|
-
- **
|
|
185
|
+
- **Local and private.** Your prompt is matched against the bundled catalog
|
|
178
186
|
on your machine; nothing is sent anywhere.
|
|
179
|
-
- **
|
|
187
|
+
- **Safe.** Never auto-installs (always your call), and fails open (a hook
|
|
180
188
|
error never blocks your prompt).
|
|
181
189
|
|
|
182
190
|
**๐ญ Capability gaps.** `skills-atlas gaps` shows Claude your *recent activity* and
|
|
183
191
|
lets **Claude** spot the recurring kinds of work you keep doing that no installed
|
|
184
|
-
skill covers yet
|
|
192
|
+
skill covers yet, then recommend one with the pattern as evidence. We don't guess
|
|
185
193
|
with heuristics; we just give Claude the memory it lacks (your recent prompts, read
|
|
186
|
-
from Claude Code's own local transcripts
|
|
194
|
+
from Claude Code's own local transcripts; **nothing is stored or sent**) plus the
|
|
187
195
|
catalog. With the hook on, it also nudges in-conversation now and then. The two
|
|
188
196
|
layers are independent: `skills-atlas hook suggest on|off` (per-prompt) and
|
|
189
197
|
`skills-atlas hook gaps on|off` (the proactive nudge).
|
package/bin/skills.js
CHANGED
|
@@ -16,12 +16,13 @@ const suggest = require('../src/commands/suggest');
|
|
|
16
16
|
const hook = require('../src/commands/hook');
|
|
17
17
|
const gaps = require('../src/commands/gaps');
|
|
18
18
|
const update = require('../src/commands/update');
|
|
19
|
+
const mcp = require('../src/commands/mcp');
|
|
19
20
|
const { categories, list } = require('../src/commands/categories');
|
|
20
21
|
|
|
21
22
|
const VERSION = require('../package.json').version;
|
|
22
23
|
// `use` = install + activate inline (emit the SKILL.md so an agent follows it now).
|
|
23
24
|
const use = argv => install([...argv, '--inline']);
|
|
24
|
-
const commands = { search, info, install, use, kit, sync, installed, upgrade, remove, outdated, doctor, suggest, hook, gaps, update, categories, list, registry };
|
|
25
|
+
const commands = { search, info, install, use, kit, sync, installed, upgrade, remove, outdated, doctor, suggest, hook, gaps, update, categories, list, registry, mcp };
|
|
25
26
|
|
|
26
27
|
const HELP = `skills-atlas โ search, install & manage AI agent skills
|
|
27
28
|
|
|
@@ -52,6 +53,9 @@ catalog:
|
|
|
52
53
|
list [category] list skill groups (optionally within one category)
|
|
53
54
|
registry add/list/remove a private catalog source (org-internal skills)
|
|
54
55
|
|
|
56
|
+
integrations:
|
|
57
|
+
mcp run as an MCP server (search/info/install/categories for any MCP client)
|
|
58
|
+
|
|
55
59
|
global flags: --zh (ไธญๆ output; English by default), --json (machine output), -h/--help
|
|
56
60
|
docs: https://zita-go.github.io/Skills-Atlas/`;
|
|
57
61
|
|
package/package.json
CHANGED
package/src/args.js
CHANGED
|
@@ -31,11 +31,17 @@ async function list(argv) {
|
|
|
31
31
|
|
|
32
32
|
const en = !values.zh;
|
|
33
33
|
const filter = (positionals.join(' ') || values.category || '').trim();
|
|
34
|
+
// A bare number means "that numbered category" โ match the "N." prefix exactly,
|
|
35
|
+
// so `list 4` doesn't also pull in "14." via loose substring.
|
|
36
|
+
const numFilter = /^\d+$/.test(filter) ? new RegExp(`^${filter}\\b`) : null;
|
|
34
37
|
const { data } = loadData({ quiet: values.json });
|
|
35
38
|
|
|
36
39
|
const out = [];
|
|
37
40
|
for (const s of data.sections) {
|
|
38
|
-
|
|
41
|
+
const hit = !filter
|
|
42
|
+
|| (numFilter ? (numFilter.test(s.title_en || '') || numFilter.test(s.title || ''))
|
|
43
|
+
: (loose(s.title, filter) || loose(s.title_en, filter)));
|
|
44
|
+
if (!hit) continue;
|
|
39
45
|
out.push({
|
|
40
46
|
section: en ? (s.title_en || s.title) : s.title,
|
|
41
47
|
groups: s.subsections.flatMap(ss => ss.rows.map(r => ({
|
package/src/commands/doctor.js
CHANGED
|
@@ -34,8 +34,8 @@ module.exports = async function doctor(argv) {
|
|
|
34
34
|
const dir = path.join(root, skill);
|
|
35
35
|
if (!fsu.dirExists(dir)) { findings.push({ scope: s.name, skill, level: 'warn', msg: 'recorded but folder missing on disk' }); continue; }
|
|
36
36
|
if (!fs.existsSync(path.join(dir, 'SKILL.md'))) findings.push({ scope: s.name, skill, level: 'error', msg: 'no SKILL.md in folder' });
|
|
37
|
-
if (!data.vendors[e.source] || !rowsFor(idx.skillIndex, skill).length) findings.push({ scope: s.name, skill, level: 'info', msg:
|
|
38
|
-
if (e.scripts) findings.push({ scope: s.name, skill, level: 'info', msg: `ships ${e.scripts} script file(s) โ review` });
|
|
37
|
+
if (!data.vendors[e.source] || !rowsFor(idx.skillIndex, skill).length) findings.push({ scope: s.name, skill, level: 'info', msg: `no longer in the catalog (keep it, or: skills-atlas remove ${skill})` });
|
|
38
|
+
if (e.scripts) findings.push({ scope: s.name, skill, level: 'info', msg: `ships ${e.scripts} script file(s) โ review before trusting: ${fsu.tildify(dir)}` });
|
|
39
39
|
const vendor = data.vendors[e.source];
|
|
40
40
|
if (vendor && vendor.license && RISKY_LICENSE.test(vendor.license)) findings.push({ scope: s.name, skill, level: 'warn', msg: `license: ${vendor.license}` });
|
|
41
41
|
}
|
package/src/commands/hook.js
CHANGED
|
@@ -45,10 +45,16 @@ module.exports = async function hook(argv) {
|
|
|
45
45
|
const ap = registry.getAutopilot();
|
|
46
46
|
if (values.json) { console.log(JSON.stringify({ enabled: on, suggest: ap.suggest, gapAlerts: ap.gapAlerts, settings: p })); return; }
|
|
47
47
|
console.log(`autopilot hook: ${on ? green('on') : dim('off')} ${dim(p)}`);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
if (on) {
|
|
49
|
+
console.log(` per-prompt suggest: ${ap.suggest ? green('on') : dim('off')} ${dim('(skills-atlas hook suggest on|off)')}`);
|
|
50
|
+
console.log(` gap alerts: ${ap.gapAlerts ? green('on') : dim('off')} ${dim('(skills-atlas hook gaps on|off)')}`);
|
|
51
|
+
console.log(dim(' review gaps: skills-atlas gaps'));
|
|
52
|
+
} else {
|
|
53
|
+
// Hook isn't registered, so these sub-toggles don't do anything yet โ don't
|
|
54
|
+
// imply the autopilot is running. Show them dimmed with the caveat.
|
|
55
|
+
console.log(dim(` (suggest ${ap.suggest ? 'on' : 'off'}, gap alerts ${ap.gapAlerts ? 'on' : 'off'} โ they take effect once you run 'skills-atlas hook on')`));
|
|
56
|
+
console.log(dim('enable: skills-atlas hook on'));
|
|
57
|
+
}
|
|
52
58
|
return;
|
|
53
59
|
}
|
|
54
60
|
|
|
@@ -71,6 +77,7 @@ module.exports = async function hook(argv) {
|
|
|
71
77
|
if (arr.some(isOurs)) { console.log(dim('autopilot already on.')); return; }
|
|
72
78
|
settings.hooks.UserPromptSubmit = [...arr, { matcher: '*', hooks: [{ type: 'command', command: HOOK_CMD, timeout: 5 }] }];
|
|
73
79
|
} else {
|
|
80
|
+
if (!arr.some(isOurs)) { console.log(dim('autopilot already off.')); return; }
|
|
74
81
|
const kept = arr.filter(e => !isOurs(e));
|
|
75
82
|
if (kept.length) settings.hooks.UserPromptSubmit = kept;
|
|
76
83
|
else delete settings.hooks.UserPromptSubmit;
|
|
@@ -80,7 +87,9 @@ module.exports = async function hook(argv) {
|
|
|
80
87
|
try {
|
|
81
88
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
82
89
|
if (fs.existsSync(p) && !fs.existsSync(`${p}.bak`)) fs.copyFileSync(p, `${p}.bak`); // keep the pristine original
|
|
83
|
-
|
|
90
|
+
// Removing our only setting? Don't leave a bare `{}` behind โ drop the file.
|
|
91
|
+
if (sub === 'off' && !Object.keys(settings).length && fs.existsSync(p)) fs.rmSync(p, { force: true });
|
|
92
|
+
else fs.writeFileSync(p, JSON.stringify(settings, null, 2) + '\n');
|
|
84
93
|
} catch (e) { console.error(`failed to write ${p}: ${e.message}`); process.exitCode = 1; return; }
|
|
85
94
|
|
|
86
95
|
if (values.json) { console.log(JSON.stringify({ enabled: sub === 'on', settings: p })); return; }
|
package/src/commands/info.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const { parse } = require('../args');
|
|
4
4
|
const { loadData } = require('../data');
|
|
5
5
|
const { buildIndices, suggestSkills } = require('../index-build');
|
|
6
|
-
const { buildInfo, renderInfo } = require('../format');
|
|
6
|
+
const { buildInfo, renderInfo, dim } = require('../format');
|
|
7
7
|
|
|
8
8
|
const HELP = `usage: skills-atlas info <skill> [--all] [--json] [--zh]
|
|
9
9
|
|
|
@@ -18,6 +18,7 @@ module.exports = async function info(argv) {
|
|
|
18
18
|
const name = positionals[0];
|
|
19
19
|
if (!name) {
|
|
20
20
|
console.error('usage: skills-atlas info <skill>');
|
|
21
|
+
console.error(dim('find a skill name first: skills-atlas search <keyword>'));
|
|
21
22
|
process.exitCode = 1;
|
|
22
23
|
return;
|
|
23
24
|
}
|
package/src/commands/install.js
CHANGED
|
@@ -25,6 +25,7 @@ options:
|
|
|
25
25
|
-y, --yes non-interactive (auto-pick top source, assume yes)
|
|
26
26
|
--chain install the whole โ workflow this skill belongs to
|
|
27
27
|
--dry-run show what would download, write nothing
|
|
28
|
+
--verbose with \`use\`: also print the full SKILL.md to the terminal
|
|
28
29
|
--json machine-readable output
|
|
29
30
|
|
|
30
31
|
Downloads the repo archive (no GitHub API rate limit). Only if that fetch fails
|
|
@@ -86,12 +87,13 @@ async function installChain({ row, vendor, src, targetRoot, values }) {
|
|
|
86
87
|
|
|
87
88
|
module.exports = async function install(argv) {
|
|
88
89
|
const { values, positionals } = parse(argv,
|
|
89
|
-
['global', 'project', 'source', 'force', 'yes', 'chain', 'inline', 'dry-run', 'json']);
|
|
90
|
+
['global', 'project', 'source', 'force', 'yes', 'chain', 'inline', 'dry-run', 'verbose', 'json']);
|
|
90
91
|
if (values.help) { console.log(HELP); return; }
|
|
91
92
|
|
|
92
93
|
const name = positionals[0];
|
|
93
94
|
if (!name) {
|
|
94
95
|
console.error('usage: skills-atlas install <skill> [--global|--project] [--source <id>] [--force] [--yes]');
|
|
96
|
+
console.error(dim('find a skill first: skills-atlas search <keyword>'));
|
|
95
97
|
process.exitCode = 1;
|
|
96
98
|
return;
|
|
97
99
|
}
|
|
@@ -126,23 +128,26 @@ module.exports = async function install(argv) {
|
|
|
126
128
|
`${c.source.name} ${stars(c.source.stars)} ${c.source.type || ''} ${(c.source.install && c.source.install.command) || ''}`);
|
|
127
129
|
const i = await choose(`'${name}' is available from ${candidates.length} sources:`, labels);
|
|
128
130
|
if (i < 0) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
+
// Non-interactive without a pick: show each source so the user can decide
|
|
132
|
+
// (stars / type / install command), instead of just listing bare names.
|
|
133
|
+
console.error(`'${name}' has ${candidates.length} sources โ pick one with --source <id>, or --yes to auto-pick the top:`);
|
|
134
|
+
candidates.forEach(c => console.error(dim(
|
|
135
|
+
` ${c.source.name} ${stars(c.source.stars)} ${c.source.type || ''}` +
|
|
136
|
+
`${(c.source.install && c.source.install.command) ? ' โ ' + c.source.install.command : ''}`)));
|
|
131
137
|
process.exitCode = 2;
|
|
132
138
|
return;
|
|
133
139
|
}
|
|
134
140
|
chosen = candidates[i];
|
|
135
141
|
}
|
|
136
142
|
|
|
137
|
-
// Heads-up
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
143
|
+
// Heads-up whenever --yes auto-picked among several sources โ say which one and
|
|
144
|
+
// what else was available, so a silent pick of a lower-star (but installable)
|
|
145
|
+
// source over a higher-star whole-repo one is never a surprise.
|
|
146
|
+
if (values.yes && candidates.length > 1 && !values.json) {
|
|
147
|
+
const st = stars(chosen.source.stars);
|
|
148
|
+
const grp = chosen.row && chosen.row.group;
|
|
149
|
+
const others = candidates.filter(c => c !== chosen).map(c => c.source.name).join(', ');
|
|
150
|
+
console.error(dim(`note: '${name}' has ${candidates.length} sources; auto-picked ${chosen.source.name}${st ? ' ' + st : ''}${grp ? ` (${grp})` : ''}. also from: ${others} โ use --source <id> to choose.`));
|
|
146
151
|
}
|
|
147
152
|
|
|
148
153
|
const v = chosen.vendor || {};
|
|
@@ -179,6 +184,9 @@ module.exports = async function install(argv) {
|
|
|
179
184
|
}
|
|
180
185
|
|
|
181
186
|
const targetRoot = fsu.installTargetDir({ global: resolveGlobal(values) });
|
|
187
|
+
// Global installs read best as ~/โฆ; a --project path is shorter and clearer as a
|
|
188
|
+
// relative ./.claude/skills/<skill> than a long absolute path.
|
|
189
|
+
const showPath = pth => resolveGlobal(values) ? fsu.tildify(pth) : './' + path.relative(process.cwd(), pth);
|
|
182
190
|
const isChain = Boolean(chosen.row && chosen.row.chain && (chosen.row.skills || []).length >= 2);
|
|
183
191
|
|
|
184
192
|
// --- chain: install the whole workflow ---
|
|
@@ -228,7 +236,7 @@ module.exports = async function install(argv) {
|
|
|
228
236
|
}));
|
|
229
237
|
return;
|
|
230
238
|
}
|
|
231
|
-
console.log(`would install ${listing.files.length} file(s) to ${
|
|
239
|
+
console.log(`would install ${listing.files.length} file(s) to ${showPath(dest)} (branch ${listing.branchUsed}):`);
|
|
232
240
|
listing.files.forEach(f => console.log(` ${f.rel}`));
|
|
233
241
|
if (listing.note) console.log(dim(' ' + listing.note));
|
|
234
242
|
console.log(dim(' (real install fetches the repo archive โ no GitHub API rate limit)'));
|
|
@@ -255,7 +263,7 @@ module.exports = async function install(argv) {
|
|
|
255
263
|
return;
|
|
256
264
|
}
|
|
257
265
|
|
|
258
|
-
console.log(`\n${green('โ')} installed ${bold(skill)} โ ${
|
|
266
|
+
console.log(`\n${green('โ')} installed ${bold(skill)} โ ${showPath(result.dest)} ${dim(`(${result.fileCount} file(s) from ${src.name}@${result.branchUsed})`)}`);
|
|
259
267
|
if (result.note) console.log(dim(' ' + result.note));
|
|
260
268
|
console.log(dim(` source: ${src.name}@${result.branchUsed} โ branch HEAD, not a pinned commit; review before use`));
|
|
261
269
|
if (result.scripts.length) {
|
|
@@ -274,13 +282,19 @@ module.exports = async function install(argv) {
|
|
|
274
282
|
|
|
275
283
|
if (values.inline) {
|
|
276
284
|
const body = readSkillMd(result.dest);
|
|
277
|
-
|
|
285
|
+
const mdPath = showPath(path.join(result.dest, 'SKILL.md'));
|
|
286
|
+
// Claude (and any piped caller) needs the full SKILL.md inline to apply the
|
|
287
|
+
// skill right now; a human at a terminal just needs the digest โ the file is
|
|
288
|
+
// on disk and auto-loads, so dumping ~200 lines would only bury the next step.
|
|
289
|
+
const full = body && (values.verbose || !process.stdout.isTTY);
|
|
290
|
+
if (full) {
|
|
278
291
|
console.log('\n' + dim('โโโ SKILL.md โ the skill\'s own instructions; apply them to the task now โโโ'));
|
|
279
292
|
console.log(body.trim());
|
|
280
293
|
console.log(dim('โโโ end SKILL.md โโโ'));
|
|
281
294
|
}
|
|
282
|
-
console.log(`\n${green('โ')} ${bold(skill)} is now active โ use the instructions above for the task at hand.`);
|
|
283
|
-
console.log(dim(` installed at ${
|
|
295
|
+
console.log(`\n${green('โ')} ${bold(skill)} is now active โ ${full ? 'use the instructions above' : 'follow its SKILL.md'} for the task at hand.`);
|
|
296
|
+
console.log(dim(` installed at ${showPath(result.dest)} (auto-loads in new sessions) ยท what it does: skills-atlas info ${skill} ยท remove: skills-atlas remove ${skill}`));
|
|
297
|
+
if (body && !full) console.log(dim(` full instructions: ${mdPath} (or re-run with --verbose)`));
|
|
284
298
|
} else {
|
|
285
299
|
console.log(`\n${bold(skill)} is installed but not loaded yet. To use it:`);
|
|
286
300
|
console.log(dim(` โข now, in this session: skills-atlas use ${skill}`));
|
package/src/commands/remove.js
CHANGED
|
@@ -19,8 +19,15 @@ module.exports = async function remove(argv) {
|
|
|
19
19
|
const dest = path.join(root, name);
|
|
20
20
|
const tracked = manifest.read(root).skills[name];
|
|
21
21
|
|
|
22
|
+
// Is the skill also present in the OTHER scope? (remove only touches one scope.)
|
|
23
|
+
const otherRoot = fsu.installTargetDir({ global: !global });
|
|
24
|
+
const otherFlag = global ? '--project' : '--global';
|
|
25
|
+
const otherHas = otherRoot !== root
|
|
26
|
+
&& (fsu.dirExists(path.join(otherRoot, name)) || !!manifest.read(otherRoot).skills[name]);
|
|
27
|
+
|
|
22
28
|
if (!fsu.dirExists(dest) && !tracked) {
|
|
23
29
|
console.error(`'${name}' is not installed (${global ? 'global' : 'project'}).`);
|
|
30
|
+
if (otherHas) console.error(dim(`it is installed in the ${global ? 'project' : 'global'} scope โ try: skills-atlas remove ${name} ${otherFlag}`));
|
|
24
31
|
process.exitCode = 1;
|
|
25
32
|
return;
|
|
26
33
|
}
|
|
@@ -35,4 +42,5 @@ module.exports = async function remove(argv) {
|
|
|
35
42
|
|
|
36
43
|
if (values.json) { console.log(JSON.stringify({ removed: name, dest })); return; }
|
|
37
44
|
console.log(`${green('โ')} removed ${name} ${dim(fsu.tildify(dest))}`);
|
|
45
|
+
if (otherHas) console.log(dim(`note: '${name}' is still installed in the ${global ? 'project' : 'global'} scope โ remove that too: skills-atlas remove ${name} ${otherFlag}`));
|
|
38
46
|
};
|
package/src/commands/search.js
CHANGED
|
@@ -12,7 +12,8 @@ Matches by words, not whole-string: multiple keywords and loose phrases work
|
|
|
12
12
|
(e.g. "pdf ็ฟป่ฏ", "translate a whole pdf"). Ranked by how much of the query hits.
|
|
13
13
|
|
|
14
14
|
filters:
|
|
15
|
-
-c, --category <s> match a top-level category (loose
|
|
15
|
+
-c, --category <s> match a top-level category (loose; zh or en; e.g. -c marketing).
|
|
16
|
+
run \`skills-atlas categories\` for the exact names
|
|
16
17
|
-p, --persona <s> match a persona โ English or Chinese, e.g.
|
|
17
18
|
engineering, pm, design, marketing, research, ops,
|
|
18
19
|
founder, job-seeking, general (ๅทฅ็จ/PM/่ฎพ่ฎก/่ฅ้/...)
|
|
@@ -62,7 +63,10 @@ module.exports = async function search(argv) {
|
|
|
62
63
|
const shown = rows.slice(0, limit);
|
|
63
64
|
|
|
64
65
|
if (values.json) {
|
|
65
|
-
console.log(JSON.stringify(
|
|
66
|
+
console.log(JSON.stringify({
|
|
67
|
+
query, total: rows.length, shown: shown.length, weak,
|
|
68
|
+
results: shown.map(jsonRow),
|
|
69
|
+
}, null, 2));
|
|
66
70
|
return;
|
|
67
71
|
}
|
|
68
72
|
|
|
@@ -73,15 +77,18 @@ module.exports = async function search(argv) {
|
|
|
73
77
|
const { skillIndex } = buildIndices(data);
|
|
74
78
|
const sugg = suggestSkills(skillIndex, query);
|
|
75
79
|
if (sugg.length) console.log(dim(`did you mean: ${sugg.join(', ')}`));
|
|
76
|
-
console.log(dim('try fewer / different words, or `skills-atlas categories` to browse.'));
|
|
77
80
|
}
|
|
81
|
+
if (values.category) console.log(dim(`if -c "${values.category}" isn't matching, run \`skills-atlas categories\` for the exact names.`));
|
|
82
|
+
console.log(dim('try fewer / different words, or `skills-atlas categories` to browse.'));
|
|
78
83
|
return;
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
if (weak) {
|
|
82
|
-
console.log(dim('\nโ
|
|
87
|
+
console.log(dim('\nโ partial match โ results may cover only part of your query; the top ones can still help, or try different words.'));
|
|
83
88
|
}
|
|
84
89
|
shown.forEach(r => console.log(renderRow(r, { en })));
|
|
85
|
-
const
|
|
86
|
-
console.log(`\n${rows.length} match(es)${
|
|
90
|
+
const truncated = rows.length > limit;
|
|
91
|
+
console.log(`\n${rows.length} match(es)${truncated ? `, showing ${limit}` : ''}.`);
|
|
92
|
+
if (truncated) console.log(dim(`see the rest with --limit ${rows.length}${values.category ? '' : ', or narrow with -c <category>'}.`));
|
|
93
|
+
console.log(dim('next: skills-atlas info <skill> to learn more, or skills-atlas use <skill> to install it.'));
|
|
87
94
|
};
|
package/src/commands/update.js
CHANGED
package/src/commands/upgrade.js
CHANGED
|
@@ -32,8 +32,15 @@ module.exports = async function upgrade(argv) {
|
|
|
32
32
|
for (const n of names) targets.push({ scope: s, name: n, entry: m.skills[n] });
|
|
33
33
|
}
|
|
34
34
|
if (!targets.length) {
|
|
35
|
-
if (values.json) { console.log('[]'); return; }
|
|
36
|
-
|
|
35
|
+
if (values.json) { console.log('[]'); if (positionals[0]) process.exitCode = 1; return; }
|
|
36
|
+
if (positionals[0]) {
|
|
37
|
+
// A named skill that isn't installed is a user error โ non-zero, like `remove`.
|
|
38
|
+
console.log(`'${positionals[0]}' is not installed by skills-atlas.`);
|
|
39
|
+
console.log(dim('see what is: skills-atlas installed'));
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
} else {
|
|
42
|
+
console.log('nothing installed to upgrade.');
|
|
43
|
+
}
|
|
37
44
|
return;
|
|
38
45
|
}
|
|
39
46
|
|
package/src/data.js
CHANGED
|
@@ -31,8 +31,17 @@ function tryReadJSON(p) {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// Structural validation, not just "has the two top keys". The catalog is iterated
|
|
35
|
+
// as sections[].subsections[].rows[] everywhere (counts/merge/search), so a source
|
|
36
|
+
// missing that shape (e.g. an old `subgroups`/`groups` schema) must be rejected
|
|
37
|
+
// HERE โ both to fail `registry add` cleanly and to keep a stale bad cache from
|
|
38
|
+
// being merged as an overlay and crashing every downstream command.
|
|
34
39
|
function isValid(d) {
|
|
35
|
-
return d
|
|
40
|
+
return !!d
|
|
41
|
+
&& Array.isArray(d.sections)
|
|
42
|
+
&& d.sections.every(s => s && Array.isArray(s.subsections)
|
|
43
|
+
&& s.subsections.every(ss => ss && Array.isArray(ss.rows)))
|
|
44
|
+
&& !!d.vendors && typeof d.vendors === 'object';
|
|
36
45
|
}
|
|
37
46
|
|
|
38
47
|
function counts(d) {
|
|
@@ -63,7 +72,10 @@ function loadBundled() {
|
|
|
63
72
|
'or run `skills-atlas update` to fetch the catalog.');
|
|
64
73
|
}
|
|
65
74
|
|
|
75
|
+
const nudgeFile = () => path.join(cacheDir(), 'nudge.json');
|
|
76
|
+
|
|
66
77
|
// One-line stderr nudge when the catalog is the bundled snapshot or a stale cache.
|
|
78
|
+
// Throttled to once per day so it informs without nagging on every command.
|
|
67
79
|
function maybeStaleNudge(fromCache) {
|
|
68
80
|
const meta = tryReadJSON(metaFile());
|
|
69
81
|
let stale = true;
|
|
@@ -71,9 +83,14 @@ function maybeStaleNudge(fromCache) {
|
|
|
71
83
|
const ageDays = (Date.now() - Date.parse(meta.fetchedAt)) / 86400000;
|
|
72
84
|
stale = !(ageDays >= 0 && ageDays < STALE_DAYS);
|
|
73
85
|
}
|
|
74
|
-
if (stale)
|
|
75
|
-
|
|
76
|
-
|
|
86
|
+
if (!stale) return;
|
|
87
|
+
try {
|
|
88
|
+
const n = tryReadJSON(nudgeFile());
|
|
89
|
+
if (n && n.at && (Date.now() - Date.parse(n.at)) < 86400000) return; // shown within the day
|
|
90
|
+
fs.mkdirSync(cacheDir(), { recursive: true });
|
|
91
|
+
fs.writeFileSync(nudgeFile(), JSON.stringify({ at: new Date().toISOString() }));
|
|
92
|
+
} catch { /* throttle I/O is best-effort */ }
|
|
93
|
+
process.stderr.write("tip: run 'skills-atlas update' to refresh the catalog\n");
|
|
77
94
|
}
|
|
78
95
|
|
|
79
96
|
function loadData({ quiet = false } = {}) {
|
|
@@ -188,4 +205,4 @@ async function refreshSources() {
|
|
|
188
205
|
return out;
|
|
189
206
|
}
|
|
190
207
|
|
|
191
|
-
module.exports = { loadData, refreshData, fetchSource, refreshSources, counts, cacheDir, PUBLIC_URL };
|
|
208
|
+
module.exports = { loadData, refreshData, fetchSource, refreshSources, counts, isValid, cacheDir, PUBLIC_URL };
|
package/src/format.js
CHANGED
|
@@ -41,7 +41,12 @@ function renderRow(r, { en = false } = {}) {
|
|
|
41
41
|
const lines = [`\n${chain}${bold(text(r, 'group', en))} ${dim('[' + cat + ']')}`];
|
|
42
42
|
const uc = text(r, 'use_case', en);
|
|
43
43
|
if (uc) lines.push(` ๐ก ${uc}`);
|
|
44
|
-
|
|
44
|
+
const SK_MAX = 8;
|
|
45
|
+
const sk = r.skills || [];
|
|
46
|
+
const skStr = sk.length > SK_MAX
|
|
47
|
+
? green(sk.slice(0, SK_MAX).join(', ')) + dim(` +${sk.length - SK_MAX} more`)
|
|
48
|
+
: green(sk.join(', '));
|
|
49
|
+
lines.push(` ${dim('skills:')} ${skStr}`);
|
|
45
50
|
const best = [...(r.sources || [])].sort((a, b) => (b.stars || 0) - (a.stars || 0))[0];
|
|
46
51
|
if (best) {
|
|
47
52
|
const inst = best.install && best.install.command ? ` โ ${best.install.command}` : '';
|
|
@@ -162,5 +167,5 @@ function renderInfo(info, { en = false, all = false } = {}) {
|
|
|
162
167
|
|
|
163
168
|
module.exports = {
|
|
164
169
|
bold, dim, green, cyan, yellow, stars, safeAlt, text, renderRow,
|
|
165
|
-
buildInfo, infoForRow, renderInfo,
|
|
170
|
+
buildInfo, infoForRow, renderInfo, PERSONA_EN,
|
|
166
171
|
};
|
package/src/mcp.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// Hand-rolled, zero-dep MCP (Model Context Protocol) server exposing the catalog
|
|
2
|
+
// over stdio (newline-delimited JSON-RPC 2.0). `handle()` is a pure dispatcher
|
|
3
|
+
// (catalog injected) for tests; `start()` wires stdin/stdout. stdout carries ONLY
|
|
4
|
+
// protocol JSON โ never console.log here.
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { loadData } = require('./data');
|
|
9
|
+
const { buildIndices, vendorsFor, skillDocPath, suggestSkills } = require('./index-build');
|
|
10
|
+
const { runSearch } = require('./search-core');
|
|
11
|
+
const { buildInfo, PERSONA_EN } = require('./format');
|
|
12
|
+
const fsu = require('./fsutil');
|
|
13
|
+
const { installFolder } = require('./installer');
|
|
14
|
+
|
|
15
|
+
const VERSION = require('../package.json').version;
|
|
16
|
+
const PROTOCOL = '2025-06-18';
|
|
17
|
+
|
|
18
|
+
// ---- tool result formatters (plain text, no ANSI) ----
|
|
19
|
+
function fmtSearch(data, { query, limit }) {
|
|
20
|
+
const { flatRows } = buildIndices(data);
|
|
21
|
+
const { rows } = runSearch(flatRows, { query });
|
|
22
|
+
if (!rows.length) return `No skills match "${query}".`;
|
|
23
|
+
const n = Math.min(rows.length, (Number.isInteger(limit) && limit > 0) ? limit : 10);
|
|
24
|
+
const out = rows.slice(0, n).map(r => {
|
|
25
|
+
const src = [...(r.sources || [])].sort((a, b) => (b.stars || 0) - (a.stars || 0))[0] || {};
|
|
26
|
+
const uc = r.use_case_en || r.use_case || '';
|
|
27
|
+
const cmd = src.install && src.install.command ? src.install.command : '';
|
|
28
|
+
return `โข ${(r.skills || []).join(', ')} [${r.group_en || r.group}]` +
|
|
29
|
+
(uc ? `\n ${uc}` : '') +
|
|
30
|
+
(src.name ? `\n via ${src.name} โ
${src.stars || 0}${cmd ? ` โ ${cmd}` : ''}` : '');
|
|
31
|
+
});
|
|
32
|
+
const more = n < rows.length ? `\n\n(showing ${n} of ${rows.length}; pass limit:${rows.length} to see all)` : '';
|
|
33
|
+
return `${rows.length} result(s) for "${query}" (showing ${n}):\n\n${out.join('\n\n')}${more}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function fmtInfo(data, { skill }) {
|
|
37
|
+
const idx = buildIndices(data);
|
|
38
|
+
const info = buildInfo(skill, { skillIndex: idx.skillIndex, vendors: data.vendors });
|
|
39
|
+
if (!info.found) {
|
|
40
|
+
const sugg = suggestSkills(idx.skillIndex, skill);
|
|
41
|
+
return `Skill "${skill}" not found.${sugg.length ? ` Did you mean: ${sugg.join(', ')}.` : ''}`;
|
|
42
|
+
}
|
|
43
|
+
const g = info.groups[0];
|
|
44
|
+
const lines = [`${skill} [${g.category_en || g.category}] โ group: ${g.group_en || g.group}`];
|
|
45
|
+
const desc = g.description_en || g.description; if (desc) lines.push(desc);
|
|
46
|
+
const uc = g.use_case_en || g.use_case; if (uc) lines.push(`use case: ${uc}`);
|
|
47
|
+
const wt = g.when_to_use_en || g.when_to_use; if (wt) lines.push(`when: ${wt}`);
|
|
48
|
+
if (g.personas && g.personas.length) lines.push(`personas: ${g.personas.map(p => PERSONA_EN[p] || p).join(', ')}`);
|
|
49
|
+
lines.push('sources:');
|
|
50
|
+
for (const s of g.sources) {
|
|
51
|
+
lines.push(` - ${s.id} โ
${s.stars || 0}${s.license ? ` (${s.license})` : ''} ${s.path || '(whole-repo install)'}`);
|
|
52
|
+
if (s.install && s.install.command) lines.push(` install: ${s.install.command}`);
|
|
53
|
+
}
|
|
54
|
+
if (info.groups.length > 1) lines.push(`(+${info.groups.length - 1} other group(s) with a same-named skill)`);
|
|
55
|
+
return lines.join('\n');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function fmtCategories(data, { category }) {
|
|
59
|
+
if (!category) {
|
|
60
|
+
const out = data.sections.map(s => {
|
|
61
|
+
const groups = s.subsections.reduce((m, ss) => m + ss.rows.length, 0);
|
|
62
|
+
return `โข ${s.title_en || s.title} (${groups} groups)`;
|
|
63
|
+
});
|
|
64
|
+
return `Categories:\n${out.join('\n')}`;
|
|
65
|
+
}
|
|
66
|
+
const lc = String(category).toLowerCase();
|
|
67
|
+
const sec = data.sections.find(s =>
|
|
68
|
+
(s.title_en || '').toLowerCase().includes(lc) || (s.title || '').toLowerCase().includes(lc));
|
|
69
|
+
if (!sec) return `Category "${category}" not found. Run list_categories with no argument to see them.`;
|
|
70
|
+
const groups = sec.subsections.flatMap(ss => ss.rows.map(r => ` โข ${r.group_en || r.group} โ ${(r.skills || []).join(', ')}`));
|
|
71
|
+
return `${sec.title_en || sec.title}:\n${groups.join('\n')}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function doInstall(data, { skill, scope, source, dry_run }) {
|
|
75
|
+
const idx = buildIndices(data);
|
|
76
|
+
const candidates = vendorsFor(idx.skillIndex, skill);
|
|
77
|
+
if (!candidates.length) {
|
|
78
|
+
const sugg = suggestSkills(idx.skillIndex, skill);
|
|
79
|
+
return { text: `Skill "${skill}" not found.${sugg.length ? ` Did you mean: ${sugg.join(', ')}.` : ''}`, isError: true };
|
|
80
|
+
}
|
|
81
|
+
const chosen = source
|
|
82
|
+
? candidates.find(c => c.source.name.toLowerCase() === String(source).toLowerCase())
|
|
83
|
+
: candidates[0];
|
|
84
|
+
if (!chosen) return { text: `Source "${source}" does not provide "${skill}". Available: ${candidates.map(c => c.source.name).join(', ')}.`, isError: true };
|
|
85
|
+
const v = chosen.vendor, src = chosen.source;
|
|
86
|
+
const name = chosen.skill || skill;
|
|
87
|
+
const docPath = skillDocPath(v, name);
|
|
88
|
+
if (!docPath) {
|
|
89
|
+
const cmd = (src.install || v.install || {}).command;
|
|
90
|
+
return { text: `"${skill}" from ${src.name} installs the whole repo, not a single folder.${cmd ? ` Run: ${cmd}` : ''}` };
|
|
91
|
+
}
|
|
92
|
+
const global = scope !== 'project';
|
|
93
|
+
const targetRoot = fsu.installTargetDir({ global });
|
|
94
|
+
const dest = path.join(targetRoot, name);
|
|
95
|
+
if (dry_run) return { text: `[dry run] would install "${skill}" from ${src.name} โ ${fsu.tildify(dest)} (no files written).` };
|
|
96
|
+
try {
|
|
97
|
+
const r = await installFolder({
|
|
98
|
+
author: v.author || src.author, repo: v.repo || src.repo,
|
|
99
|
+
branch: v.default_branch || src.default_branch || 'main',
|
|
100
|
+
docPath, dest, targetRoot, skillName: name,
|
|
101
|
+
source: src.name, group: chosen.row && chosen.row.group, category: chosen.row && chosen.row._cat,
|
|
102
|
+
});
|
|
103
|
+
return { text: `Installed "${skill}" โ ${r.fileCount} file(s) from ${src.name}@${r.branchUsed} โ ${fsu.tildify(r.dest)}.${r.scripts.length ? ` Includes ${r.scripts.length} script file(s); review before use.` : ''}` };
|
|
104
|
+
} catch (e) {
|
|
105
|
+
return { text: `Install failed: ${e.message}`, isError: true };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function toolDefs() {
|
|
110
|
+
return [
|
|
111
|
+
{ name: 'search_skills', description: 'Search the Skills Atlas catalog of AI agent skills by keyword or short task description. To install a result, call install_skill with the skill name โ do not run the npx/install command shown in the results (that is the source reference, often a whole-repo install).', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'keywords or a short task description' }, limit: { type: 'integer', minimum: 1, description: 'max results (default 10)' } }, required: ['query'] } },
|
|
112
|
+
{ name: 'skill_info', description: 'Show what a catalog skill does, when to use it, its sources, and how to install it.', inputSchema: { type: 'object', properties: { skill: { type: 'string', description: 'exact skill name (e.g. brainstorming); fuzzy matching and typo correction are applied' } }, required: ['skill'] } },
|
|
113
|
+
{ name: 'install_skill', description: 'Install a skill from the catalog into .claude/skills/.', inputSchema: { type: 'object', properties: { skill: { type: 'string' }, scope: { type: 'string', enum: ['global', 'project'], description: 'default global (~/.claude/skills)' }, source: { type: 'string', description: 'pick a source when several provide the skill' }, dry_run: { type: 'boolean' } }, required: ['skill'] } },
|
|
114
|
+
{ name: 'list_categories', description: "List the catalog's top-level categories, or the skill groups within one.", inputSchema: { type: 'object', properties: { category: { type: 'string', description: 'optional โ drill into one category' } } } },
|
|
115
|
+
];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function callTool(name, args, data) {
|
|
119
|
+
args = args || {};
|
|
120
|
+
switch (name) {
|
|
121
|
+
case 'search_skills':
|
|
122
|
+
if (!args.query) return { text: 'missing required "query".', isError: true };
|
|
123
|
+
if (args.limit !== undefined && (!Number.isInteger(args.limit) || args.limit < 1))
|
|
124
|
+
return { text: `invalid "limit": ${JSON.stringify(args.limit)} (must be a positive integer).`, isError: true };
|
|
125
|
+
return { text: fmtSearch(data, args) };
|
|
126
|
+
case 'skill_info':
|
|
127
|
+
if (!args.skill) return { text: 'missing required "skill".', isError: true };
|
|
128
|
+
return { text: fmtInfo(data, args) };
|
|
129
|
+
case 'list_categories':
|
|
130
|
+
return { text: fmtCategories(data, args) };
|
|
131
|
+
case 'install_skill':
|
|
132
|
+
if (!args.skill) return { text: 'missing required "skill".', isError: true };
|
|
133
|
+
return await doInstall(data, args);
|
|
134
|
+
default:
|
|
135
|
+
return { text: `unknown tool: ${name}`, isError: true };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function handle(req, { data }) {
|
|
140
|
+
const id = req && req.id;
|
|
141
|
+
const ok = result => ({ jsonrpc: '2.0', id, result });
|
|
142
|
+
const err = (code, message) => ({ jsonrpc: '2.0', id, error: { code, message } });
|
|
143
|
+
if (!req || req.jsonrpc !== '2.0') return id === undefined ? null : err(-32600, 'Invalid Request');
|
|
144
|
+
try {
|
|
145
|
+
switch (req.method) {
|
|
146
|
+
case 'initialize':
|
|
147
|
+
return ok({ protocolVersion: (req.params && req.params.protocolVersion) || PROTOCOL, capabilities: { tools: {} }, serverInfo: { name: 'skills-atlas', version: VERSION } });
|
|
148
|
+
case 'notifications/initialized':
|
|
149
|
+
case 'initialized':
|
|
150
|
+
return null;
|
|
151
|
+
case 'tools/list':
|
|
152
|
+
return ok({ tools: toolDefs() });
|
|
153
|
+
case 'tools/call': {
|
|
154
|
+
const p = req.params || {};
|
|
155
|
+
const r = await callTool(p.name, p.arguments, data);
|
|
156
|
+
return ok({ content: [{ type: 'text', text: r.text }], isError: Boolean(r.isError) });
|
|
157
|
+
}
|
|
158
|
+
default:
|
|
159
|
+
return id === undefined ? null : err(-32601, 'Method not found');
|
|
160
|
+
}
|
|
161
|
+
} catch (e) {
|
|
162
|
+
// A request with an id MUST get a response โ never leave the client hanging.
|
|
163
|
+
return id === undefined ? null : err(-32603, (e && e.message) || 'Internal error');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function start() {
|
|
168
|
+
let data;
|
|
169
|
+
try { data = loadData({ quiet: true }).data; }
|
|
170
|
+
catch (e) { process.stderr.write(`skills-atlas mcp: ${e.message}\n`); process.exit(1); return; }
|
|
171
|
+
let buf = '';
|
|
172
|
+
let queue = Promise.resolve();
|
|
173
|
+
process.stdin.setEncoding('utf8');
|
|
174
|
+
process.stdin.on('data', chunk => {
|
|
175
|
+
buf += chunk;
|
|
176
|
+
let nl;
|
|
177
|
+
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
178
|
+
const line = buf.slice(0, nl).trim(); buf = buf.slice(nl + 1);
|
|
179
|
+
if (!line) continue;
|
|
180
|
+
let req; try { req = JSON.parse(line); } catch { continue; }
|
|
181
|
+
queue = queue.then(async () => {
|
|
182
|
+
try { const res = await handle(req, { data }); if (res) process.stdout.write(JSON.stringify(res) + '\n'); }
|
|
183
|
+
catch { /* never crash the server */ }
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
process.stdin.on('end', () => { queue.then(() => process.exit(0)); });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
module.exports = { handle, toolDefs, callTool, start };
|
package/src/search-core.js
CHANGED
|
@@ -23,8 +23,16 @@ const STOP = new Set([
|
|
|
23
23
|
'ๅจ', 'ๆฏ', 'ๅฐฑ', 'ไน', '้ฝ', '่ฟ', '่ฎฉ', '่ท', 'ๅฏน', 'ๅ', 'ๆไปฌ', 'ๅธฎๆ', 'ๆๆณ',
|
|
24
24
|
]);
|
|
25
25
|
|
|
26
|
+
// Leading interrogative / filler phrases ("ๅฆไฝโฆ", "how do iโฆ", "i want toโฆ") add
|
|
27
|
+
// noise tokens โ including a bridging CJK bigram ("ๅฆไฝ้ๆ" โ keeps "ไฝ้") โ that
|
|
28
|
+
// dilute coverage and wrongly flag a good query as weak. Strip them up front.
|
|
29
|
+
const LEAD_FILLER_EN = /^(?:\s*(?:how\s+(?:do|can|should)\s+(?:i|we|you)|how\s+to|i\s+(?:want|need|wanna)\s+to|i\s+would\s+like\s+to|can\s+you|could\s+you|please|help\s+me|let'?s)\s+)+/;
|
|
30
|
+
const LEAD_FILLER_ZH = /^(?:ๅฆไฝ|ๆไนๆ ท|ๆๆ ท|ๆไน|่ฏท้ฎ|ๅธฎๆ็็|ๅธฎๆ|ๆๆณ่ฆ|ๆๆณ|ๆ่ฆ|ๆ้่ฆ|้บป็ฆ)+/;
|
|
31
|
+
const stripLeadingFillers = q => q.replace(LEAD_FILLER_EN, '').replace(LEAD_FILLER_ZH, '');
|
|
32
|
+
|
|
26
33
|
// Split a (lowercased) query into search terms.
|
|
27
34
|
function tokenize(query) {
|
|
35
|
+
query = stripLeadingFillers(query);
|
|
28
36
|
const out = new Set();
|
|
29
37
|
const re = /[a-z0-9]+|[ไธ-้ฟฟ]+/g;
|
|
30
38
|
let m;
|
|
@@ -249,12 +257,19 @@ function suggestCandidates(rows, prompt, { installed = new Set(), suggested = ne
|
|
|
249
257
|
const push = (skill, row) => { if (!seen.has(skill)) { seen.add(skill); out.push({ skill, row }); } };
|
|
250
258
|
for (const a of anchors) { if (out.length >= limit) break; push(a.skill, a.row); }
|
|
251
259
|
|
|
252
|
-
// 2. fill from the general ranked search
|
|
260
|
+
// 2. fill remaining slots from the general ranked search โ but only with rows
|
|
261
|
+
// that are actually on-topic (a name/group hit, or strong coverage). A lone
|
|
262
|
+
// description-word match must not pad the shortlist with off-topic skills
|
|
263
|
+
// (e.g. "product-marketing" riding into a "debug this crash" prompt).
|
|
253
264
|
const { rows: ranked, weak } = runSearch(rows, { query: prompt });
|
|
254
265
|
for (const r of ranked) {
|
|
255
266
|
if (out.length >= limit) break;
|
|
256
267
|
const s = (r.skills || []).find(x => !seen.has(x));
|
|
257
|
-
if (s)
|
|
268
|
+
if (!s) continue;
|
|
269
|
+
const f = buildFields(r);
|
|
270
|
+
const nameOrGroup = tokens.some(t => fieldHas(f.name, t) || fieldHas(f.group, t));
|
|
271
|
+
if (!nameOrGroup && scoreRow(r, tokens, lc(prompt)).coverage < 0.6) continue;
|
|
272
|
+
push(s, r);
|
|
258
273
|
}
|
|
259
274
|
|
|
260
275
|
// fire decision: only on a real name anchor โ two contentful name words, OR one
|