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 CHANGED
@@ -3,57 +3,43 @@
3
3
  [![npm](https://img.shields.io/npm/v/skills-atlas-cli)](https://www.npmjs.com/package/skills-atlas-cli)
4
4
  [![license](https://img.shields.io/npm/l/skills-atlas-cli)](https://github.com/Zita-Go/Skills-Atlas/blob/main/LICENSE)
5
5
 
6
- **Stop guessing which agent skill to use.** Find, install, and learn the right
7
- specialized skill for the task โ€” in seconds, straight into Claude Code.
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
- Powered by the [**Skills Atlas**](https://zita-go.github.io/Skills-Atlas/) catalog:
10
- hundreds of Claude Code / Codex skills across 100+ source repos, organized by what
11
- they actually do.
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
- <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">
14
-
15
- ## ๐ŸŒ Browse the whole catalog online
14
+ ### Two ways to get the right skill
16
15
 
17
- <table>
18
- <tr>
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
- **[โ†’ zita-go.github.io/Skills-Atlas](https://zita-go.github.io/Skills-Atlas/)** โ€”
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
- ## Install
21
+ ## Quickstart
28
22
 
29
23
  ```bash
30
- npm install -g skills-atlas-cli # adds the `skills-atlas` command (alias: `sa`)
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
- ## How you'll use it
36
-
37
- Three ways to reach the same catalog โ€” pick whichever fits the moment:
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
- | Mode | Best when | Get going |
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
- **60-second quickstart:**
33
+ ## ๐ŸŒ Browse the catalog online
46
34
 
47
- ```bash
48
- npm install -g skills-atlas-cli
49
- skills-atlas search "stress test my launch plan" # โ†’ pre-mortem tops the results
50
- skills-atlas use pre-mortem # install + activate now (prints its SKILL.md)
51
- skills-atlas hook on # optional โ€” let the right skill find you from here on
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
- `use` drops the skill into `~/.claude/skills/` and prints it for the task at hand; it
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 AND activate now โ€” prints SKILL.md, no restart
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
- # ๐ŸŒ Catalog
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 โ€” a universal dev workflow plus archetype
99
- add-ons โ€” into `./.claude/skills/`, then writes a committable `skills-atlas.kit.json`.
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 โ€” function-organized, bilingual, tagged with
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 โ€” **no GitHub API rate limit**) into
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 โ€” `--source <id>` to
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 โ€” a `data.json` in the same
131
- schema โ€” so internal skills show up in `search` / `info` / `install` / `kit`
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
- just describe what you need, or use `/skills-atlas:skill-search`, `:skill-info`,
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
- ## Autopilot (opt-in) โ€” the right skill finds you
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 โ€” and
165
- if so, explains **what it does and why it fits your task**, then offers a choice:
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
- - **off by default** โ€” you turn it on explicitly; `hook off` removes it cleanly.
173
- - **quiet** โ€” only fires on a distinctive match (greetings and generic actions
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 โ€” and Claude is the
183
+ the same skill twice, with a cooldown between suggestions. Claude is the
176
184
  final filter on relevance.
177
- - **local & private** โ€” your prompt is matched against the bundled catalog
185
+ - **Local and private.** Your prompt is matched against the bundled catalog
178
186
  on your machine; nothing is sent anywhere.
179
- - **safe** โ€” never auto-installs (always your call), and fails open (a hook
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 โ€” then recommend one, with the pattern as evidence. We don't guess
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 โ€” **nothing is stored or sent**) plus the
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skills-atlas-cli",
3
- "version": "0.7.0",
3
+ "version": "0.8.2",
4
4
  "description": "Search, install and learn AI agent skills from the terminal โ€” powered by the Skills Atlas catalog.",
5
5
  "bin": {
6
6
  "skills-atlas": "bin/skills.js",
package/src/args.js CHANGED
@@ -21,6 +21,7 @@ const ALL = {
21
21
  zh: { type: 'boolean' },
22
22
  all: { type: 'boolean' },
23
23
  inline: { type: 'boolean' },
24
+ verbose: { type: 'boolean' },
24
25
  archetype: { type: 'string' },
25
26
  update: { type: 'boolean' },
26
27
  name: { type: 'string' },
@@ -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
- if (filter && !(loose(s.title, filter) || loose(s.title_en, filter))) continue;
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 => ({
@@ -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: 'no longer in the catalog' });
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
  }
@@ -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
- console.log(` per-prompt suggest: ${ap.suggest ? green('on') : dim('off')} ${dim('(skills-atlas hook suggest on|off)')}`);
49
- console.log(` gap alerts: ${ap.gapAlerts ? green('on') : dim('off')} ${dim('(skills-atlas hook gaps on|off)')}`);
50
- if (on) console.log(dim(' review gaps: skills-atlas gaps'));
51
- if (!on) console.log(dim('enable: skills-atlas hook on'));
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
- fs.writeFileSync(p, JSON.stringify(settings, null, 2) + '\n');
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; }
@@ -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
  }
@@ -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
- console.error(`multiple sources for '${name}'. re-run with --source <id> or --yes.`);
130
- console.error(`sources: ${candidates.map(c => c.source.name).join(', ')}`);
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 when --yes auto-picked among semantically different groups.
138
- if (values.yes && candidates.length > 1) {
139
- const groups = [...new Set(candidates.map(c => c.row && c.row.group).filter(Boolean))];
140
- if (groups.length > 1) {
141
- const picked = chosen.row && chosen.row.group;
142
- const otherGroups = groups.filter(g => g !== picked).join('; ');
143
- const st = stars(chosen.source.stars);
144
- console.error(dim(`note: '${name}' exists in ${groups.length} groups; auto-picked ${chosen.source.name}${st ? ' ' + st : ''} (${picked || '?'}). also in: ${otherGroups} โ€” use --source to choose.`));
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 ${fsu.tildify(dest)} (branch ${listing.branchUsed}):`);
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)} โ†’ ${fsu.tildify(result.dest)} ${dim(`(${result.fileCount} file(s) from ${src.name}@${result.branchUsed})`)}`);
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
- if (body) {
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 ${fsu.tildify(result.dest)} (auto-loads in new sessions) ยท what it does: skills-atlas info ${skill} ยท remove: skills-atlas remove ${skill}`));
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}`));
@@ -0,0 +1,5 @@
1
+ // `skills-atlas mcp` โ€” start the stdio MCP server (blocks; speaks JSON-RPC 2.0 over
2
+ // stdin/stdout for an MCP client to spawn). All logic lives in ../mcp.
3
+ 'use strict';
4
+
5
+ module.exports = () => require('../mcp').start();
@@ -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
  };
@@ -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, zh or en)
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(shown.map(jsonRow), null, 2));
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โš  weak matches โ€” none of the results cover your whole query; try different words.'));
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 more = rows.length > limit ? `, showing ${limit}` : '';
86
- console.log(`\n${rows.length} match(es)${more}.`);
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
  };
@@ -28,6 +28,7 @@ module.exports = async function update(argv) {
28
28
  }
29
29
  } catch (e) {
30
30
  console.error(`update failed: ${e.message}`);
31
+ console.error('your existing catalog is unchanged and still usable offline.');
31
32
  process.exitCode = 1;
32
33
  }
33
34
  };
@@ -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
- console.log(positionals[0] ? `'${positionals[0]}' is not installed by skills-atlas.` : 'nothing installed to upgrade.');
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 && Array.isArray(d.sections) && d.vendors && typeof d.vendors === 'object';
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
- process.stderr.write("tip: run 'skills-atlas update' to refresh the catalog\n");
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
- lines.push(` ${dim('skills:')} ${green(r.skills.join(', '))}`);
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 };
@@ -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 (one primary skill per row)
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) push(s, r);
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