skills-atlas-cli 0.8.0 โ 0.8.4
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 +55 -61
- 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 +24 -2
- package/src/commands/install.js +31 -17
- package/src/commands/remove.js +8 -0
- package/src/commands/search.js +18 -7
- package/src/commands/update.js +1 -0
- package/src/commands/upgrade.js +9 -2
- package/src/data.js +22 -5
- package/src/format.js +64 -10
- package/src/mcp.js +27 -19
- 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`
|
|
34
|
-
|
|
35
|
-
## How you'll use it
|
|
24
|
+
npm install -g skills-atlas-cli # adds the `skills-atlas` command (alias sa). Or run any command with `npx`.
|
|
36
25
|
|
|
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
|
|
@@ -164,10 +158,10 @@ the catalog. Add it to your client's config:
|
|
|
164
158
|
```
|
|
165
159
|
|
|
166
160
|
It exposes four tools: **search_skills**, **skill_info**, **install_skill**, and
|
|
167
|
-
**list_categories
|
|
161
|
+
**list_categories**. Discover, inspect, install, and browse the catalog from
|
|
168
162
|
anywhere.
|
|
169
163
|
|
|
170
|
-
## Autopilot (opt-in)
|
|
164
|
+
## Autopilot (opt-in): the right skill finds you
|
|
171
165
|
|
|
172
166
|
```bash
|
|
173
167
|
skills-atlas hook on # enable (skills-atlas hook off / status)
|
|
@@ -175,29 +169,29 @@ skills-atlas hook on # enable (skills-atlas hook off / status)
|
|
|
175
169
|
|
|
176
170
|
Registers a Claude Code `UserPromptSubmit` hook. When what you ask matches the
|
|
177
171
|
territory of a catalog skill you don't have, the hook hands Claude a short
|
|
178
|
-
shortlist of candidates and **Claude decides** whether any genuinely fits
|
|
179
|
-
|
|
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:
|
|
180
174
|
use it now, see what it covers first (`skills-atlas info`), or skip. You don't
|
|
181
175
|
have to know the skill exists. The split is deliberate: the hook does **recall**
|
|
182
176
|
(a distinctive-word match against the catalog, so the right skill is on the
|
|
183
177
|
table), Claude does **precision** (it understands your intent and stays silent
|
|
184
178
|
unless one truly fits, or searches further itself). It's:
|
|
185
179
|
|
|
186
|
-
- **
|
|
187
|
-
- **
|
|
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
|
|
188
182
|
like "fix the typo" stay silent), never for an already-installed skill, never
|
|
189
|
-
the same skill twice, with a cooldown between suggestions
|
|
183
|
+
the same skill twice, with a cooldown between suggestions. Claude is the
|
|
190
184
|
final filter on relevance.
|
|
191
|
-
- **
|
|
185
|
+
- **Local and private.** Your prompt is matched against the bundled catalog
|
|
192
186
|
on your machine; nothing is sent anywhere.
|
|
193
|
-
- **
|
|
187
|
+
- **Safe.** Never auto-installs (always your call), and fails open (a hook
|
|
194
188
|
error never blocks your prompt).
|
|
195
189
|
|
|
196
190
|
**๐ญ Capability gaps.** `skills-atlas gaps` shows Claude your *recent activity* and
|
|
197
191
|
lets **Claude** spot the recurring kinds of work you keep doing that no installed
|
|
198
|
-
skill covers yet
|
|
192
|
+
skill covers yet, then recommend one with the pattern as evidence. We don't guess
|
|
199
193
|
with heuristics; we just give Claude the memory it lacks (your recent prompts, read
|
|
200
|
-
from Claude Code's own local transcripts
|
|
194
|
+
from Claude Code's own local transcripts; **nothing is stored or sent**) plus the
|
|
201
195
|
catalog. With the hook on, it also nudges in-conversation now and then. The two
|
|
202
196
|
layers are independent: `skills-atlas hook suggest on|off` (per-prompt) and
|
|
203
197
|
`skills-atlas hook gaps on|off` (the proactive nudge).
|
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
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
3
5
|
const { parse } = require('../args');
|
|
4
6
|
const { loadData } = require('../data');
|
|
5
7
|
const { buildIndices, suggestSkills } = require('../index-build');
|
|
6
|
-
const { buildInfo, renderInfo } = require('../format');
|
|
8
|
+
const { buildInfo, renderInfo, dim } = require('../format');
|
|
9
|
+
const fsu = require('../fsutil');
|
|
10
|
+
const manifest = require('../manifest');
|
|
11
|
+
|
|
12
|
+
// The authoritative one-liner: a skill's own SKILL.md `description:` frontmatter,
|
|
13
|
+
// read locally when installed (offline). Catalog text is group-level; this is not.
|
|
14
|
+
function localSkillDesc(skill) {
|
|
15
|
+
for (const s of fsu.scopesFor({})) {
|
|
16
|
+
try {
|
|
17
|
+
const md = fs.readFileSync(path.join(s.root, skill, 'SKILL.md'), 'utf8');
|
|
18
|
+
const fm = md.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
19
|
+
const dm = fm && fm[1].match(/^description:\s*(.+)$/m);
|
|
20
|
+
if (dm) return dm[1].trim().replace(/^["']|["']$/g, '');
|
|
21
|
+
} catch { /* not installed in this scope */ }
|
|
22
|
+
}
|
|
23
|
+
return '';
|
|
24
|
+
}
|
|
7
25
|
|
|
8
26
|
const HELP = `usage: skills-atlas info <skill> [--all] [--json] [--zh]
|
|
9
27
|
|
|
@@ -18,6 +36,7 @@ module.exports = async function info(argv) {
|
|
|
18
36
|
const name = positionals[0];
|
|
19
37
|
if (!name) {
|
|
20
38
|
console.error('usage: skills-atlas info <skill>');
|
|
39
|
+
console.error(dim('find a skill name first: skills-atlas search <keyword>'));
|
|
21
40
|
process.exitCode = 1;
|
|
22
41
|
return;
|
|
23
42
|
}
|
|
@@ -43,5 +62,8 @@ module.exports = async function info(argv) {
|
|
|
43
62
|
console.log(JSON.stringify(infoObj, null, 2));
|
|
44
63
|
return;
|
|
45
64
|
}
|
|
46
|
-
|
|
65
|
+
const installed = [];
|
|
66
|
+
for (const s of fsu.scopesFor({})) if (manifest.list(s.root).some(e => e.skill === infoObj.skill)) installed.push(s.name);
|
|
67
|
+
const skillDesc = installed.length ? localSkillDesc(infoObj.skill) : '';
|
|
68
|
+
console.log(renderInfo(infoObj, { en: !values.zh, all: values.all, installed, skillDesc }));
|
|
47
69
|
};
|
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
|
@@ -5,6 +5,8 @@ const { loadData } = require('../data');
|
|
|
5
5
|
const { buildIndices, suggestSkills } = require('../index-build');
|
|
6
6
|
const { runSearch } = require('../search-core');
|
|
7
7
|
const { renderRow, dim } = require('../format');
|
|
8
|
+
const fsu = require('../fsutil');
|
|
9
|
+
const manifest = require('../manifest');
|
|
8
10
|
|
|
9
11
|
const HELP = `usage: skills-atlas search <query...> [filters]
|
|
10
12
|
|
|
@@ -12,7 +14,8 @@ Matches by words, not whole-string: multiple keywords and loose phrases work
|
|
|
12
14
|
(e.g. "pdf ็ฟป่ฏ", "translate a whole pdf"). Ranked by how much of the query hits.
|
|
13
15
|
|
|
14
16
|
filters:
|
|
15
|
-
-c, --category <s> match a top-level category (loose
|
|
17
|
+
-c, --category <s> match a top-level category (loose; zh or en; e.g. -c marketing).
|
|
18
|
+
run \`skills-atlas categories\` for the exact names
|
|
16
19
|
-p, --persona <s> match a persona โ English or Chinese, e.g.
|
|
17
20
|
engineering, pm, design, marketing, research, ops,
|
|
18
21
|
founder, job-seeking, general (ๅทฅ็จ/PM/่ฎพ่ฎก/่ฅ้/...)
|
|
@@ -62,7 +65,10 @@ module.exports = async function search(argv) {
|
|
|
62
65
|
const shown = rows.slice(0, limit);
|
|
63
66
|
|
|
64
67
|
if (values.json) {
|
|
65
|
-
console.log(JSON.stringify(
|
|
68
|
+
console.log(JSON.stringify({
|
|
69
|
+
query, total: rows.length, shown: shown.length, weak,
|
|
70
|
+
results: shown.map(jsonRow),
|
|
71
|
+
}, null, 2));
|
|
66
72
|
return;
|
|
67
73
|
}
|
|
68
74
|
|
|
@@ -73,15 +79,20 @@ module.exports = async function search(argv) {
|
|
|
73
79
|
const { skillIndex } = buildIndices(data);
|
|
74
80
|
const sugg = suggestSkills(skillIndex, query);
|
|
75
81
|
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
82
|
}
|
|
83
|
+
if (values.category) console.log(dim(`if -c "${values.category}" isn't matching, run \`skills-atlas categories\` for the exact names.`));
|
|
84
|
+
console.log(dim('try fewer / different words, or `skills-atlas categories` to browse.'));
|
|
78
85
|
return;
|
|
79
86
|
}
|
|
80
87
|
|
|
81
88
|
if (weak) {
|
|
82
|
-
console.log(dim('\nโ
|
|
89
|
+
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
90
|
}
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
console.log(
|
|
91
|
+
const installedSet = new Set();
|
|
92
|
+
for (const s of fsu.scopesFor({})) for (const e of manifest.list(s.root)) installedSet.add(e.skill);
|
|
93
|
+
shown.forEach(r => console.log(renderRow(r, { en, installed: installedSet, vendors: data.vendors })));
|
|
94
|
+
const truncated = rows.length > limit;
|
|
95
|
+
console.log(`\n${rows.length} match(es)${truncated ? `, showing ${limit}` : ''}.`);
|
|
96
|
+
if (truncated) console.log(dim(`see the rest with --limit ${rows.length}${values.category ? '' : ', or narrow with -c <category>'}.`));
|
|
97
|
+
console.log(dim('next: skills-atlas info <skill> to learn more, or skills-atlas use <skill> to install it.'));
|
|
87
98
|
};
|
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
|
@@ -19,6 +19,17 @@ function stars(n) {
|
|
|
19
19
|
return `โ
${n}`;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
// Repo freshness from a source's `last_commit` ("2026-05-28"). Returns null if
|
|
23
|
+
// absent/unparseable. `stale` flags a repo untouched for over a year โ so a popular
|
|
24
|
+
// but abandoned source no longer looks identical to an actively maintained one.
|
|
25
|
+
function recency(lastCommit, now = Date.now()) {
|
|
26
|
+
if (!lastCommit) return null;
|
|
27
|
+
const t = Date.parse(lastCommit);
|
|
28
|
+
if (!Number.isFinite(t)) return null;
|
|
29
|
+
const days = Math.floor((now - t) / 86400000);
|
|
30
|
+
return { date: String(lastCommit).slice(0, 10), days, stale: days > 365 };
|
|
31
|
+
}
|
|
32
|
+
|
|
22
33
|
// A `git clone <repo> ~/.claude/skills/skills` alt double-nests a multi-skill
|
|
23
34
|
// monorepo (skills end up at .claude/skills/skills/<name>/) and won't load โ
|
|
24
35
|
// drop that foot-gun; the `npx skills add` command is the correct path.
|
|
@@ -34,18 +45,33 @@ function text(obj, key, en) {
|
|
|
34
45
|
return en ? (obj[key + '_en'] || obj[key] || '') : (obj[key] || obj[key + '_en'] || '');
|
|
35
46
|
}
|
|
36
47
|
|
|
37
|
-
// One search/list result line block.
|
|
38
|
-
|
|
48
|
+
// One search/list result line block. `installed` (a Set of skill names) marks what
|
|
49
|
+
// you already have; `vendors` lets the source line say whether install is a clean
|
|
50
|
+
// single-skill folder or a whole-repo command.
|
|
51
|
+
function renderRow(r, { en = false, installed = null, vendors = null } = {}) {
|
|
39
52
|
const chain = r.chain ? cyan('โ ') : '';
|
|
40
53
|
const cat = en ? (r._catEn || r._cat) : r._cat;
|
|
41
54
|
const lines = [`\n${chain}${bold(text(r, 'group', en))} ${dim('[' + cat + ']')}`];
|
|
42
55
|
const uc = text(r, 'use_case', en);
|
|
43
56
|
if (uc) lines.push(` ๐ก ${uc}`);
|
|
44
|
-
|
|
57
|
+
const SK_MAX = 8;
|
|
58
|
+
const sk = r.skills || [];
|
|
59
|
+
const tick = s => (installed && installed.has(s)) ? green(s + ' โ') : green(s);
|
|
60
|
+
const skStr = sk.length > SK_MAX
|
|
61
|
+
? sk.slice(0, SK_MAX).map(tick).join(dim(', ')) + dim(` +${sk.length - SK_MAX} more`)
|
|
62
|
+
: sk.map(tick).join(dim(', '));
|
|
63
|
+
lines.push(` ${dim('skills:')} ${skStr}`);
|
|
45
64
|
const best = [...(r.sources || [])].sort((a, b) => (b.stars || 0) - (a.stars || 0))[0];
|
|
46
65
|
if (best) {
|
|
66
|
+
const rec = recency(best.last_commit);
|
|
67
|
+
const recStr = rec ? ` updated ${rec.date}${rec.stale ? ' โ ' : ''}` : '';
|
|
68
|
+
let kind = '';
|
|
69
|
+
if (vendors) {
|
|
70
|
+
const v = vendors[best.name];
|
|
71
|
+
kind = (v && skillDocPath(v, sk[0])) ? ' [single-skill]' : ' [whole-repo]';
|
|
72
|
+
}
|
|
47
73
|
const inst = best.install && best.install.command ? ` โ ${best.install.command}` : '';
|
|
48
|
-
lines.push(` ${dim('via')} ${best.name} ${yellow(stars(best.stars))}${dim(inst)}`);
|
|
74
|
+
lines.push(` ${dim('via')} ${best.name} ${yellow(stars(best.stars))}${dim(recStr)}${dim(kind)}${dim(inst)}`);
|
|
49
75
|
}
|
|
50
76
|
return lines.join('\n');
|
|
51
77
|
}
|
|
@@ -84,6 +110,7 @@ function groupOf(r, skillName, vendors) {
|
|
|
84
110
|
id: s.name,
|
|
85
111
|
url: s.url,
|
|
86
112
|
stars: s.stars,
|
|
113
|
+
last_commit: s.last_commit || v.last_commit || null,
|
|
87
114
|
license: (v.skill_licenses && v.skill_licenses[skillName]) || s.license || null,
|
|
88
115
|
type: s.type,
|
|
89
116
|
path: docPath || null,
|
|
@@ -123,15 +150,40 @@ function infoForRow(skillName, row, vendors) {
|
|
|
123
150
|
return { skill: skillName, found: true, groups: [groupOf(row, skillName, vendors)] };
|
|
124
151
|
}
|
|
125
152
|
|
|
126
|
-
|
|
153
|
+
// Multi-skill groups share one description that often enumerates each skill as
|
|
154
|
+
// "<vendor> <skill> (<detail>) + โฆ". Pull out the segment for THIS skill so info
|
|
155
|
+
// pinpoints what it does, not just the whole group. Conservative: only fire when
|
|
156
|
+
// the skill matches exactly one segment (otherwise we'd guess wrong).
|
|
157
|
+
function perSkillBlurb(skill, desc) {
|
|
158
|
+
if (!skill || !desc) return '';
|
|
159
|
+
const segs = String(desc).split(' + ');
|
|
160
|
+
if (segs.length < 2) return '';
|
|
161
|
+
const re = new RegExp('\\b' + String(skill).replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'i');
|
|
162
|
+
const hits = segs.filter(s => re.test(s));
|
|
163
|
+
if (hits.length !== 1) return '';
|
|
164
|
+
const seg = hits[0].trim();
|
|
165
|
+
return /[(๏ผ]/.test(seg) ? seg : ''; // only worth showing if it carries a detail
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// `skillDesc` is the authoritative one-liner from the skill's own SKILL.md (read
|
|
169
|
+
// locally when installed); it wins over the heuristic catalog-segment extraction.
|
|
170
|
+
function renderInfo(info, { en = false, all = false, installed = null, skillDesc = '' } = {}) {
|
|
127
171
|
const pick = (zh, e) => (en ? (e || zh) : (zh || e)) || '';
|
|
128
172
|
const out = [];
|
|
129
|
-
|
|
173
|
+
const instTag = installed && installed.length
|
|
174
|
+
? ` ${green('โ installed')} ${dim('[' + installed.join('+') + ']')}` : '';
|
|
175
|
+
out.push(`\n${bold(green(info.skill))}${info.groups.some(g => g.chain) ? ' ' + cyan('โ') : ''}${instTag}`);
|
|
130
176
|
const shown = all ? info.groups : info.groups.slice(0, 1);
|
|
177
|
+
let firstGroup = true;
|
|
131
178
|
for (const g of shown) {
|
|
132
179
|
out.push(` ${dim('group:')} ${pick(g.group, g.group_en)} ${dim('[' + pick(g.category, g.category_en) + ']')}`);
|
|
133
180
|
const desc = pick(g.description, g.description_en);
|
|
134
|
-
|
|
181
|
+
const blurb = (firstGroup && skillDesc) ? skillDesc : perSkillBlurb(info.skill, desc);
|
|
182
|
+
firstGroup = false;
|
|
183
|
+
const distinguished = blurb && blurb !== desc;
|
|
184
|
+
if (distinguished) out.push(` ${dim('this skill:')} ${blurb}`);
|
|
185
|
+
// Only call it "group does:" when a per-skill line is shown above it to contrast.
|
|
186
|
+
if (desc) out.push(distinguished ? ` ${dim('group does:')} ${desc}` : ` ${desc}`);
|
|
135
187
|
const uc = pick(g.use_case, g.use_case_en);
|
|
136
188
|
if (uc) out.push(` ${dim('use case:')} ${uc}`);
|
|
137
189
|
const wt = pick(g.when_to_use, g.when_to_use_en);
|
|
@@ -141,7 +193,9 @@ function renderInfo(info, { en = false, all = false } = {}) {
|
|
|
141
193
|
}
|
|
142
194
|
out.push(` ${dim('sources:')}`);
|
|
143
195
|
for (const s of g.sources) {
|
|
144
|
-
|
|
196
|
+
const rec = recency(s.last_commit);
|
|
197
|
+
const recStr = rec ? ` ${dim('updated ' + rec.date)}${rec.stale ? ' ' + yellow('โ stale') : ''}` : '';
|
|
198
|
+
out.push(` โข ${bold(s.id)} ${yellow(stars(s.stars))} ${dim(s.type || '')} ${s.license ? dim('(' + s.license + ')') : ''}${recStr}`);
|
|
145
199
|
out.push(` ${dim(s.url)}`);
|
|
146
200
|
out.push(` ${dim('path:')} ${s.path || dim('(whole-repo install โ no per-skill folder)')}`);
|
|
147
201
|
if (s.install && s.install.command) {
|
|
@@ -161,6 +215,6 @@ function renderInfo(info, { en = false, all = false } = {}) {
|
|
|
161
215
|
}
|
|
162
216
|
|
|
163
217
|
module.exports = {
|
|
164
|
-
bold, dim, green, cyan, yellow, stars, safeAlt, text, renderRow,
|
|
165
|
-
buildInfo, infoForRow, renderInfo,
|
|
218
|
+
bold, dim, green, cyan, yellow, stars, recency, perSkillBlurb, safeAlt, text, renderRow,
|
|
219
|
+
buildInfo, infoForRow, renderInfo, PERSONA_EN,
|
|
166
220
|
};
|
package/src/mcp.js
CHANGED
|
@@ -8,7 +8,7 @@ const path = require('path');
|
|
|
8
8
|
const { loadData } = require('./data');
|
|
9
9
|
const { buildIndices, vendorsFor, skillDocPath, suggestSkills } = require('./index-build');
|
|
10
10
|
const { runSearch } = require('./search-core');
|
|
11
|
-
const { buildInfo } = require('./format');
|
|
11
|
+
const { buildInfo, PERSONA_EN } = require('./format');
|
|
12
12
|
const fsu = require('./fsutil');
|
|
13
13
|
const { installFolder } = require('./installer');
|
|
14
14
|
|
|
@@ -29,7 +29,8 @@ function fmtSearch(data, { query, limit }) {
|
|
|
29
29
|
(uc ? `\n ${uc}` : '') +
|
|
30
30
|
(src.name ? `\n via ${src.name} โ
${src.stars || 0}${cmd ? ` โ ${cmd}` : ''}` : '');
|
|
31
31
|
});
|
|
32
|
-
|
|
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}`;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
function fmtInfo(data, { skill }) {
|
|
@@ -44,7 +45,7 @@ function fmtInfo(data, { skill }) {
|
|
|
44
45
|
const desc = g.description_en || g.description; if (desc) lines.push(desc);
|
|
45
46
|
const uc = g.use_case_en || g.use_case; if (uc) lines.push(`use case: ${uc}`);
|
|
46
47
|
const wt = g.when_to_use_en || g.when_to_use; if (wt) lines.push(`when: ${wt}`);
|
|
47
|
-
if (g.personas && g.personas.length) lines.push(`personas: ${g.personas.join(', ')}`);
|
|
48
|
+
if (g.personas && g.personas.length) lines.push(`personas: ${g.personas.map(p => PERSONA_EN[p] || p).join(', ')}`);
|
|
48
49
|
lines.push('sources:');
|
|
49
50
|
for (const s of g.sources) {
|
|
50
51
|
lines.push(` - ${s.id} โ
${s.stars || 0}${s.license ? ` (${s.license})` : ''} ${s.path || '(whole-repo install)'}`);
|
|
@@ -107,8 +108,8 @@ async function doInstall(data, { skill, scope, source, dry_run }) {
|
|
|
107
108
|
|
|
108
109
|
function toolDefs() {
|
|
109
110
|
return [
|
|
110
|
-
{ name: 'search_skills', description: 'Search the Skills Atlas catalog of AI agent skills by keyword or short task description.', 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'] } },
|
|
111
|
-
{ 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' } }, required: ['skill'] } },
|
|
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'] } },
|
|
112
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'] } },
|
|
113
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' } } } },
|
|
114
115
|
];
|
|
@@ -119,6 +120,8 @@ async function callTool(name, args, data) {
|
|
|
119
120
|
switch (name) {
|
|
120
121
|
case 'search_skills':
|
|
121
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 };
|
|
122
125
|
return { text: fmtSearch(data, args) };
|
|
123
126
|
case 'skill_info':
|
|
124
127
|
if (!args.skill) return { text: 'missing required "skill".', isError: true };
|
|
@@ -138,21 +141,26 @@ async function handle(req, { data }) {
|
|
|
138
141
|
const ok = result => ({ jsonrpc: '2.0', id, result });
|
|
139
142
|
const err = (code, message) => ({ jsonrpc: '2.0', id, error: { code, message } });
|
|
140
143
|
if (!req || req.jsonrpc !== '2.0') return id === undefined ? null : err(-32600, 'Invalid Request');
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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');
|
|
153
160
|
}
|
|
154
|
-
|
|
155
|
-
|
|
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');
|
|
156
164
|
}
|
|
157
165
|
}
|
|
158
166
|
|
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
|