skill-tags 1.2.1 → 1.5.0

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
@@ -16,6 +16,7 @@ If you're interested in contributing to Cursor Kits, please let me know!
16
16
  - [Agent Setup Prompt](#agent-setup-prompt)
17
17
  - [Install Options](#install-options)
18
18
  - [How It Works](#how-it-works)
19
+ - [Project-Level Install](#project-level-install)
19
20
  - [CLI Reference](#cli-reference)
20
21
  - [Manual Sync](#manual-sync)
21
22
  - [Skill Sources Scanned](#skill-sources-scanned)
@@ -23,29 +24,36 @@ If you're interested in contributing to Cursor Kits, please let me know!
23
24
  - [How Categorization Works](#how-categorization-works)
24
25
  - [Uninstall](#uninstall)
25
26
  - [Requirements](#requirements)
27
+ - [Changelog](#changelog)
26
28
  - [License](#license)
27
29
 
28
30
  ---
29
31
 
30
32
  ## Quick Start
31
33
 
34
+ 1. **Install:** `npm install skill-tags -g` (or `--save-dev` for project-level)
35
+ 2. **Follow the prompts** — choose **Global** or **Project**, then **Auto** or **Manual** sync
36
+ 3. **Use:** Reference `@skill-tags.md` in any Cursor chat
37
+
32
38
  ```bash
33
- # Install
34
39
  npm install skill-tags -g
35
-
36
- # Add the shell auto-trigger wrapper
37
- skill-tags --setup
40
+ # Setup wizard runs automatically — choose Global + Auto (recommended)
38
41
  source ~/.zshrc
42
+ ```
39
43
 
40
- # Initial sync (generate command file)
41
- skill-tags
44
+ For project-level installs:
45
+
46
+ ```bash
47
+ npm install skill-tags --save-dev
48
+ # Setup wizard runs automatically — choose Project + Auto
49
+ npm run skills add owner/repo/skill-name # adds skill + auto-syncs
42
50
  ```
43
51
 
44
52
  ## Usage
45
53
 
46
54
  In any Cursor chat, attach the full index of your skills by referencing the generated command file:
47
55
 
48
- - `@skill-tags.md`
56
+ - `@skill-tags.md` (global) or `@project-skill-tags.md` (local, from `skill-tags --local`)
49
57
 
50
58
  Example:
51
59
 
@@ -63,20 +71,25 @@ For more focused context windows, you can group your skills into category-specif
63
71
  skill-tags --categories
64
72
  ```
65
73
 
66
- This opens an interactive wizard where you can create, edit, and delete categories. Skill assignment is powered by keyword matching against the generated `skill-tags.md` index — all text is lowercased with strict full-word matching and automatic suffix expansion (plurals, gerunds, etc.) to avoid false positives.
67
-
68
- Suggested skills are pre-selected (`[*]`). Use arrow keys and space to toggle any skill before confirming.
74
+ This opens an interactive wizard where you can add and remove categories. On save, skill assignment is powered by keyword matching against the generated `skill-tags.md` index — all text is lowercased with strict full-word matching and automatic suffix expansion (plurals, gerunds, etc.) to avoid false positives.
69
75
 
70
76
  Once configured, every `skill-tags` sync automatically regenerates the category files from the saved config at `~/.cursor/skill-tags-categories.conf`.
71
77
 
78
+ For project-level categories (using only skills in `.agents/skills/`):
79
+
80
+ ```bash
81
+ skill-tags --categories --local
82
+ ```
83
+
84
+ Then reference focused category indexes instead of the full list:
85
+
72
86
  ```bash
73
- # Inject a focused category index instead of the full list:
74
87
  @skills-frontend.md
75
88
  @skills-testing.md
76
89
  @skills-ai-agents.md
77
90
  ```
78
91
 
79
- Predefined categories: `frontend`, `backend`, `database`, `testing`, `design`, `accessibility`, `performance`, `ai-agents`, `devops`, `marketing`, `mobile`, `documentation`. You can also create custom categories with any name.
92
+ Predefined categories: `frontend`, `backend`, `database`, `testing`, `design`, `accessibility`, `performance`, `ai-agents`, `devops`, `marketing`, `mobile`, `documentation`.
80
93
 
81
94
  ---
82
95
 
@@ -93,10 +106,8 @@ Install and configure the `skill-tags` package (npm: https://www.npmjs.com/packa
93
106
  Use a terminal that runs outside the sandbox with full permissions to avoid permission errors during install. In Cursor, this means using a non-sandboxed terminal session if available (required_permissions: ["all"] if running via agent shell tools).
94
107
 
95
108
  Steps to perform:
96
- 1. Install the package: `npm install skill-tags -g`
97
- 2. Run `skill-tags --setup` to install the `skills()` shell wrapper in my rc file (~/.zshrc or ~/.bash_profile)
98
- 3. Run `skill-tags` to perform an initial sync of all installed skills
99
- 4. Source my shell rc file or instruct me to do so manually
109
+ 1. Install the package: `npm install skill-tags -g` — the setup wizard runs automatically during install; when prompted, choose Global and Auto (recommended)
110
+ 2. Source my shell rc file or instruct me to do so manually
100
111
 
101
112
  When complete, output a summary that includes:
102
113
  - Where the command file was generated (~/.cursor/commands/skill-tags.md)
@@ -113,14 +124,21 @@ When complete, output a summary that includes:
113
124
 
114
125
  ## Install Options
115
126
 
116
- ### Install via npm
127
+ ### Global install (npm)
117
128
 
118
129
  ```bash
119
- npm install skill-tags -g
120
- skill-tags --setup
130
+ npm install skill-tags -g # setup wizard runs automatically
121
131
  source ~/.zshrc
122
132
  ```
123
133
 
134
+ ### Project install (npm)
135
+
136
+ ```bash
137
+ npm install skill-tags --save-dev # setup wizard runs automatically
138
+ ```
139
+
140
+ Adds `"skills": "st-skills"` to `package.json`. Use `npm run skills add <pkg>` to add project skills — auto-syncs `.cursor/commands/project-skill-tags.md` on every change.
141
+
124
142
  ### One-off run (no install)
125
143
 
126
144
  ```bash
@@ -128,7 +146,7 @@ npx skill-tags
128
146
  ```
129
147
 
130
148
  ### Install via curl
131
-
149
+
132
150
  ```bash
133
151
  curl -fsSL https://raw.githubusercontent.com/steve-piece/skill-tags/main/install.sh | bash
134
152
  ```
@@ -141,31 +159,76 @@ source ~/.zshrc # or ~/.bash_profile / ~/.bashrc
141
159
 
142
160
  ## How It Works
143
161
 
144
- After setup, the `skills` command wraps `npx skills` and automatically runs a sync after every `skills add` or `skills remove`:
162
+ After global setup, the `skills` command wraps `npx skills` and automatically runs a sync after every `skills add` or `skills remove`:
145
163
 
146
164
  ```bash
147
165
  # Install (or remove) a skill — sync runs automatically
148
166
  skills add vercel-labs/agent-skills/vercel-react-best-practices
149
167
 
150
- # The single command file is updated:
168
+ # The command file is updated:
151
169
  # ~/.cursor/commands/skill-tags.md
152
170
 
153
171
  # Use it in Cursor chat:
154
172
  # @skill-tags.md
155
173
  ```
156
174
 
175
+ ## Project-Level Install
176
+
177
+ Install skill-tags as a dev dependency to manage project-specific skills without touching your global shell config. The setup wizard runs automatically during install and adds a `"skills"` npm script backed by the bundled `st-skills` binary.
178
+
179
+ ```bash
180
+ npm install skill-tags --save-dev
181
+ # choose: Project + Auto when prompted
182
+ ```
183
+
184
+ After setup, `package.json` will include:
185
+
186
+ ```json
187
+ "scripts": {
188
+ "skills": "st-skills"
189
+ }
190
+ ```
191
+
192
+ Use `npm run skills` to add, remove, or update project skills:
193
+
194
+ ```bash
195
+ npm run skills add owner/repo/skill-name # adds skill + auto-syncs
196
+ npm run skills remove owner/repo/skill-name # removes skill + auto-syncs
197
+ npm run skills update owner/repo/skill-name # updates skill + auto-syncs
198
+
199
+ # Manual re-sync:
200
+ skill-tags --local
201
+
202
+ # Use in Cursor chat:
203
+ # @project-skill-tags.md
204
+ ```
205
+
206
+ The `st-skills` binary (from this package) wraps `npx skills`, then automatically runs `skill-tags --local` on every successful `add`, `remove`, or `update`, writing the index to `.cursor/commands/project-skill-tags.md`.
207
+
157
208
  ## CLI Reference
158
209
 
159
210
  ```bash
160
- skill-tags # sync all skills, generate/update the command file
161
- skill-tags --categories # open interactive category wizard (CRUD)
162
- skill-tags --setup # install skills() shell wrapper in ~/.zshrc
163
- skill-tags --global-only # skip project-level skills (.agents/skills in CWD)
164
- skill-tags --project-only # scan only .agents/skills in CWD; write to .cursor/commands/skill-tags.md
165
- skill-tags --version # print version
166
- skill-tags --help # show usage
211
+ skill-tags # sync all skills, generate/update the command file
212
+ skill-tags --categories # open interactive category wizard
213
+ skill-tags --categories --local # category wizard for project-level skills only
214
+ skill-tags --setup # re-run setup wizard (runs automatically on first install)
215
+ skill-tags --global # skip local skills (.agents/skills in CWD); scan global sources only
216
+ skill-tags --local # scan only .agents/skills in CWD; write to .cursor/commands/project-skill-tags.md
217
+ skill-tags --latest # update skill-tags to the latest version
218
+ skill-tags --version # print version
219
+ skill-tags --help # show usage
220
+ ```
221
+
222
+ ### `st-skills` (project binary)
223
+
224
+ ```bash
225
+ npm run skills add <pkg> # install a project skill + auto-sync
226
+ npm run skills remove <pkg> # remove a project skill + auto-sync
227
+ npm run skills update <pkg> # update a project skill + auto-sync
167
228
  ```
168
229
 
230
+ `st-skills` is registered in `package.json` by `skill-tags --setup` (local mode). It wraps `npx skills` and calls `skill-tags --local` after every mutating command.
231
+
169
232
  ## Manual Sync
170
233
 
171
234
  Run at any time to regenerate the command file:
@@ -192,7 +255,7 @@ Skills are discovered from all of these locations automatically. When the same s
192
255
 
193
256
  ## Generated File Format
194
257
 
195
- The `~/.cursor/commands/skill-tags.md` contains:
258
+ The `~/.cursor/commands/skill-tags.md` (global) and `./.cursor/commands/project-skill-tags.md` (local) contain:
196
259
 
197
260
  - An opening instruction for the agent to assess all skills and apply them autonomously.
198
261
  - Instructions for the agent on how to plan with skills when in Plan Mode.
@@ -227,6 +290,47 @@ bash uninstall.sh
227
290
  - bash or zsh
228
291
  - [Cursor IDE](https://cursor.com)
229
292
 
293
+ ## Changelog
294
+
295
+ ### v1.5.0
296
+
297
+ - **`--latest` flag** — run `skill-tags --latest` to check npm for updates and self-update in place
298
+ - **Project-level categories** — `skill-tags --categories --local` runs the category wizard using only project skills (`.agents/skills/`), stores config in `.cursor/skill-tags-categories.conf`, and generates category files in `.cursor/commands/`
299
+ - **Simplified category wizard** — streamlined to three actions: Add categories (multi-select from predefined list), Edit categories (multi-select to remove), and Save changes (runs keyword matching on save)
300
+ - **Quieter sync from wizard** — `sync.sh` accepts `--quiet` to suppress verbose scan output; the category wizard uses it for cleaner post-save output with an aligned summary table
301
+ - **Removed custom category input** — categories are now limited to the predefined list for consistency
302
+
303
+ ### v1.4.0
304
+
305
+ - Interactive `--categories` wizard with keyword-based auto-assignment
306
+ - Category config persistence at `~/.cursor/skill-tags-categories.conf`
307
+ - Auto-regeneration of category files on every sync
308
+ - Predefined categories: frontend, backend, database, testing, design, accessibility, performance, ai-agents, devops, marketing, mobile, documentation
309
+
310
+ ### v1.3.0
311
+
312
+ - `--local` flag for project-level skill scanning
313
+ - `st-skills` binary for project-level add/remove/update with auto-sync
314
+ - Project setup mode in `--setup` wizard
315
+
316
+ ### v1.2.0
317
+
318
+ - `--global` flag to skip local skills
319
+ - `--setup` wizard with Global and Project modes
320
+ - Shell wrapper auto-sync on `skills add/remove`
321
+
322
+ ### v1.1.0
323
+
324
+ - Deduplication across skill sources (first-found wins by priority)
325
+ - Support for nested plugin cache directories
326
+ - Description extraction from YAML frontmatter and first content line
327
+
328
+ ### v1.0.0
329
+
330
+ - Initial release
331
+ - Scans all known skill locations and generates `~/.cursor/commands/skill-tags.md`
332
+ - Priority-ordered skill sources with deduplication
333
+
230
334
  ## License
231
335
 
232
336
  MIT
package/bin/categories.js CHANGED
@@ -5,18 +5,26 @@
5
5
 
6
6
  'use strict';
7
7
 
8
- const { checkbox, select, input, confirm } = require('@inquirer/prompts');
8
+ const { checkbox, select } = require('@inquirer/prompts');
9
9
  const { spawnSync } = require('child_process');
10
10
  const path = require('path');
11
11
  const fs = require('fs');
12
12
  const os = require('os');
13
13
 
14
14
  const HOME = os.homedir();
15
- const CATEGORIES_CONFIG = path.join(HOME, '.cursor', 'skill-tags-categories.conf');
16
- const COMMANDS_DIR = path.join(HOME, '.cursor', 'commands');
17
- const SKILL_TAGS_FILE = path.join(COMMANDS_DIR, 'skill-tags.md');
15
+ const IS_LOCAL = process.argv.includes('--local');
18
16
  const SYNC_SCRIPT = path.join(__dirname, '..', 'sync.sh');
19
17
 
18
+ const CATEGORIES_CONFIG = IS_LOCAL
19
+ ? path.join(process.cwd(), '.cursor', 'skill-tags-categories.conf')
20
+ : path.join(HOME, '.cursor', 'skill-tags-categories.conf');
21
+ const COMMANDS_DIR = IS_LOCAL
22
+ ? path.join(process.cwd(), '.cursor', 'commands')
23
+ : path.join(HOME, '.cursor', 'commands');
24
+ const SKILL_TAGS_FILE = IS_LOCAL
25
+ ? path.join(process.cwd(), '.cursor', 'commands', 'project-skill-tags.md')
26
+ : path.join(HOME, '.cursor', 'commands', 'skill-tags.md');
27
+
20
28
  const PREDEFINED_CATEGORIES = [
21
29
  'frontend',
22
30
  'backend',
@@ -119,8 +127,10 @@ const CATEGORY_KEYWORDS = {
119
127
 
120
128
  function ensureSkillTagsFile() {
121
129
  if (fs.existsSync(SKILL_TAGS_FILE)) return;
122
- console.log(' skill-tags.md not found — running initial sync...\n');
123
- spawnSync('bash', [SYNC_SCRIPT], { stdio: 'inherit' });
130
+ const label = IS_LOCAL ? 'project-skill-tags.md' : 'skill-tags.md';
131
+ console.log(` ${label} not found running initial sync...\n`);
132
+ const args = IS_LOCAL ? [SYNC_SCRIPT, '--local'] : [SYNC_SCRIPT];
133
+ spawnSync('bash', args, { stdio: 'inherit' });
124
134
  }
125
135
 
126
136
  function loadSkillsFromIndex() {
@@ -212,135 +222,66 @@ function toTitleCase(str) {
212
222
  return str.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
213
223
  }
214
224
 
215
- function truncate(str, len) {
216
- if (!str || str.length <= len) return str || '';
217
- return str.slice(0, len - 1) + '…';
218
- }
219
-
220
225
  // ─── Wizard actions ──────────────────────────────────────────────────────────
221
226
 
222
- async function addCategories(skills, config) {
227
+ async function addCategories(config) {
223
228
  const existing = new Set(Object.keys(config));
224
229
  const available = PREDEFINED_CATEGORIES.filter(c => !existing.has(c));
225
230
 
226
- let selected = [];
227
- if (available.length > 0) {
228
- selected = await checkbox({
229
- message: 'Select categories to add (space to toggle)',
230
- choices: available.map(c => ({ name: toTitleCase(c), value: c })),
231
- pageSize: 14,
232
- });
233
- } else {
234
- console.log('\n All predefined categories already exist.');
231
+ if (available.length === 0) {
232
+ console.log('\n All categories already added.\n');
233
+ return;
235
234
  }
236
235
 
237
- const custom = await input({
238
- message: 'Custom category name (blank to skip):',
236
+ const selected = await checkbox({
237
+ message: 'Select categories to add (space to toggle)',
238
+ choices: available.map(c => ({ name: toTitleCase(c), value: c })),
239
+ pageSize: 14,
239
240
  });
240
241
 
241
- if (custom.trim()) {
242
- const normalized = custom.trim().toLowerCase()
243
- .replace(/[^a-z0-9-]/g, '-')
244
- .replace(/-+/g, '-')
245
- .replace(/^-|-$/g, '');
246
- if (normalized && !existing.has(normalized)) {
247
- selected.push(normalized);
248
- } else if (existing.has(normalized)) {
249
- console.log(` "${normalized}" already exists — use Edit instead.`);
250
- }
251
- }
252
-
253
242
  if (selected.length === 0) {
254
243
  console.log(' No categories selected.\n');
255
244
  return;
256
245
  }
257
246
 
258
247
  for (const cat of selected) {
259
- const skillChoices = skills.map(s => {
260
- const match = matchKeywords(s, cat);
261
- const hint = match ? ` — ${match.reason}` : '';
262
- return {
263
- name: `${s.name}${hint}`,
264
- value: s.name,
265
- checked: !!match,
266
- description: truncate(s.description, 60),
267
- };
268
- });
269
-
270
- const selectedSkills = await checkbox({
271
- message: `Select skills for "${cat}" (space to toggle)`,
272
- choices: skillChoices,
273
- pageSize: 15,
274
- });
275
-
276
- config[cat] = selectedSkills;
277
- console.log(` ✓ ${toTitleCase(cat)}: ${selectedSkills.length} skill(s)\n`);
248
+ config[cat] = [];
249
+ console.log(` + ${toTitleCase(cat)}`);
278
250
  }
251
+ console.log();
279
252
  }
280
253
 
281
- async function editCategory(skills, config) {
282
- const cats = Object.keys(config);
283
- if (cats.length === 0) {
284
- console.log(' No categories yet. Add one first.\n');
285
- return;
286
- }
287
-
288
- const cat = await select({
289
- message: 'Which category to edit?',
290
- choices: cats.map(c => ({
291
- name: `${toTitleCase(c)} (${config[c].length} skills)`,
292
- value: c,
293
- })),
294
- });
295
-
296
- const currentSet = new Set(config[cat] || []);
297
- const skillChoices = skills.map(s => {
298
- const match = matchKeywords(s, cat);
299
- const hint = match ? ` — ${match.reason}` : '';
300
- return {
301
- name: `${s.name}${hint}`,
302
- value: s.name,
303
- checked: currentSet.has(s.name),
304
- description: truncate(s.description, 60),
305
- };
306
- });
307
-
308
- const selectedSkills = await checkbox({
309
- message: `Select skills for "${cat}" (space to toggle)`,
310
- choices: skillChoices,
311
- pageSize: 15,
312
- });
313
-
314
- config[cat] = selectedSkills;
315
- console.log(` ✓ Updated ${toTitleCase(cat)}: ${selectedSkills.length} skill(s)\n`);
316
- }
317
-
318
- async function deleteCategory(config) {
254
+ async function editCategories(config) {
319
255
  const cats = Object.keys(config);
320
256
  if (cats.length === 0) {
321
- console.log(' No categories yet.\n');
257
+ console.log(' No categories yet. Add some first.\n');
322
258
  return;
323
259
  }
324
260
 
325
- const cat = await select({
326
- message: 'Which category to delete?',
261
+ const kept = await checkbox({
262
+ message: 'Uncheck categories to remove (space to toggle)',
327
263
  choices: cats.map(c => ({
328
264
  name: `${toTitleCase(c)} (${config[c].length} skills)`,
329
265
  value: c,
266
+ checked: true,
330
267
  })),
268
+ pageSize: 14,
331
269
  });
332
270
 
333
- const yes = await confirm({
334
- message: `Delete "${cat}" and its generated command file?`,
335
- default: false,
336
- });
271
+ const keptSet = new Set(kept);
272
+ const removed = cats.filter(c => !keptSet.has(c));
337
273
 
338
- if (yes) {
274
+ for (const cat of removed) {
339
275
  delete config[cat];
340
276
  const genFile = path.join(COMMANDS_DIR, `skills-${cat}.md`);
341
277
  try { fs.unlinkSync(genFile); } catch {}
342
- console.log(` Deleted: ${cat}\n`);
278
+ console.log(` - Removed: ${toTitleCase(cat)}`);
279
+ }
280
+
281
+ if (removed.length === 0) {
282
+ console.log(' No changes.');
343
283
  }
284
+ console.log();
344
285
  }
345
286
 
346
287
  function printCurrentCategories(config) {
@@ -359,11 +300,13 @@ function printCurrentCategories(config) {
359
300
  // ─── Main loop ───────────────────────────────────────────────────────────────
360
301
 
361
302
  async function main() {
362
- console.log('\n skill-tags: category wizard\n');
303
+ const mode = IS_LOCAL ? 'project' : 'global';
304
+ const indexFile = IS_LOCAL ? 'project-skill-tags.md' : 'skill-tags.md';
305
+ console.log(`\n skill-tags: category wizard (${mode})\n`);
363
306
 
364
307
  ensureSkillTagsFile();
365
308
 
366
- console.log(' Loading skills from skill-tags.md...');
309
+ console.log(` Loading skills from ${indexFile}...`);
367
310
  const skills = loadSkillsFromIndex();
368
311
  console.log(` Found ${skills.length} skill(s)\n`);
369
312
 
@@ -376,30 +319,60 @@ async function main() {
376
319
  message: 'What would you like to do?',
377
320
  choices: [
378
321
  { name: 'Add categories', value: 'add' },
379
- { name: 'Edit a category', value: 'edit' },
380
- { name: 'Delete a category', value: 'delete' },
381
- { name: 'Save & generate files', value: 'save' },
322
+ { name: 'Edit categories', value: 'edit' },
323
+ { name: 'Save changes', value: 'save' },
382
324
  { name: 'Quit without saving', value: 'quit' },
383
325
  ],
384
326
  });
385
327
 
386
328
  switch (action) {
387
329
  case 'add':
388
- await addCategories(skills, config);
330
+ await addCategories(config);
389
331
  break;
390
332
  case 'edit':
391
- await editCategory(skills, config);
392
- break;
393
- case 'delete':
394
- await deleteCategory(config);
333
+ await editCategories(config);
395
334
  break;
396
335
  case 'save': {
336
+ const cats = Object.keys(config);
337
+ if (cats.length === 0) {
338
+ console.log(' No categories to save.\n');
339
+ break;
340
+ }
341
+
342
+ const pad = Math.max(...cats.map(c => toTitleCase(c).length));
343
+
344
+ console.log();
345
+ console.log(' ┌─ Matching skills to categories ─────────────────');
346
+ for (const cat of cats) {
347
+ const matched = skills.filter(s => matchKeywords(s, cat));
348
+ config[cat] = matched.map(s => s.name);
349
+ const label = toTitleCase(cat).padEnd(pad);
350
+ const count = String(matched.length).padStart(2);
351
+ console.log(` │ ✓ ${label} ${count} skill${matched.length === 1 ? '' : 's'}`);
352
+ }
353
+ console.log(' └─────────────────────────────────────────────────');
354
+
397
355
  writeConfig(config);
398
- console.log('\n Config saved. Running sync...\n');
399
- const result = spawnSync('bash', [SYNC_SCRIPT], { stdio: 'inherit' });
356
+
357
+ const syncArgs = IS_LOCAL
358
+ ? [SYNC_SCRIPT, '--local', '--quiet']
359
+ : [SYNC_SCRIPT, '--quiet'];
360
+ console.log(`\n Syncing ${skills.length} skills...\n`);
361
+ const result = spawnSync('bash', syncArgs, { stdio: 'inherit' });
400
362
  if (result.error) {
401
- console.error(` Error running sync: ${result.error.message}`);
363
+ console.error(` Error: ${result.error.message}`);
364
+ process.exit(1);
365
+ }
366
+
367
+ const configPath = IS_LOCAL
368
+ ? '.cursor/skill-tags-categories.conf'
369
+ : '~/.cursor/skill-tags-categories.conf';
370
+ console.log(` ✓ Config saved → ${configPath}`);
371
+ console.log(` ✓ ${cats.length} category file${cats.length === 1 ? '' : 's'} generated`);
372
+ for (const cat of cats) {
373
+ console.log(` @skills-${cat}.md`);
402
374
  }
375
+ console.log(`\n Tip: type @skills-<category>.md in Cursor chat to use a category.\n`);
403
376
  process.exit(result.status ?? 0);
404
377
  }
405
378
  case 'quit':
@@ -1,42 +1,82 @@
1
1
  #!/usr/bin/env node
2
2
  // bin/postinstall.js
3
- // Runs automatically after `npm install -g skill-tags`.
4
- // Performs an initial sync of any already-installed skills.
3
+ // Runs automatically after `npm install skill-tags`.
4
+ // Launches interactive setup wizard on first install; re-syncs on reinstall.
5
+ // Falls back gracefully for non-interactive environments (CI, piped input).
5
6
  // Never throws — a failed postinstall should never break npm install.
6
7
 
7
8
  'use strict';
8
9
 
9
- const { spawnSync } = require('child_process');
10
10
  const path = require('path');
11
11
  const fs = require('fs');
12
+ const os = require('os');
12
13
 
13
- // Skip entirely on Windows
14
14
  if (process.platform === 'win32') {
15
15
  process.exit(0);
16
16
  }
17
17
 
18
- // Skip on local (non-global) installs to avoid running on every `npm install`
19
- // in a project. npm sets npm_config_global=true for -g installs.
20
- if (process.env.npm_config_global !== 'true') {
21
- process.exit(0);
22
- }
18
+ const HOME = os.homedir();
19
+ const isGlobal = process.env.npm_config_global === 'true';
20
+ const SYNC_SCRIPT = path.join(__dirname, '..', 'sync.sh');
21
+ const WRAPPER_MARKER = '# ─── skill-tags / Cursor Skill Command Sync';
23
22
 
24
- const syncScript = path.join(__dirname, '..', 'sync.sh');
23
+ function isAlreadyConfigured() {
24
+ if (isGlobal) {
25
+ const shellName = path.basename(process.env.SHELL || 'bash');
26
+ let rcFile;
27
+ if (shellName === 'zsh') rcFile = path.join(HOME, '.zshrc');
28
+ else if (process.platform === 'darwin') rcFile = path.join(HOME, '.bash_profile');
29
+ else rcFile = path.join(HOME, '.bashrc');
25
30
 
26
- if (!fs.existsSync(syncScript)) {
27
- process.exit(0);
28
- }
31
+ try {
32
+ return fs.readFileSync(rcFile, 'utf-8').includes(WRAPPER_MARKER);
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
29
37
 
30
- console.log('\n skill-tags: running initial sync...\n');
38
+ try {
39
+ const pkg = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf-8'));
40
+ return pkg.scripts?.skills === 'st-skills';
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
31
45
 
32
- try {
33
- const result = spawnSync('bash', [syncScript], { stdio: 'inherit' });
34
- if (result.error) throw result.error;
35
- } catch (_) {
36
- // Never fail the install
46
+ function runSync() {
47
+ if (!fs.existsSync(SYNC_SCRIPT)) return;
48
+ const { spawnSync } = require('child_process');
49
+ spawnSync('bash', [SYNC_SCRIPT], { stdio: 'inherit' });
37
50
  }
38
51
 
39
- console.log(' To auto-sync after every `skills add`, run:');
40
- console.log(' skill-tags --setup\n');
52
+ async function main() {
53
+ if (isAlreadyConfigured()) {
54
+ if (isGlobal) {
55
+ console.log('\n skill-tags: re-syncing...\n');
56
+ runSync();
57
+ } else {
58
+ console.log('\n skill-tags: already configured.\n');
59
+ }
60
+ return;
61
+ }
62
+
63
+ try {
64
+ const runSetup = require('./setup');
65
+ await runSetup();
66
+ } catch (err) {
67
+ if (err.name === 'ExitPromptError') {
68
+ console.log();
69
+ return;
70
+ }
71
+ if (isGlobal) {
72
+ console.log('\n skill-tags: running initial sync...\n');
73
+ runSync();
74
+ console.log(' To configure auto-sync, run: skill-tags --setup\n');
75
+ } else {
76
+ console.log('\n skill-tags installed as a project dependency.');
77
+ console.log(' Run setup to configure: npx skill-tags --setup\n');
78
+ }
79
+ }
80
+ }
41
81
 
42
- process.exit(0);
82
+ main();
package/bin/setup.js ADDED
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env node
2
+ // bin/setup.js
3
+ // Interactive setup wizard: global/project install mode + auto/manual sync preference.
4
+
5
+ 'use strict';
6
+
7
+ const { spawnSync } = require('child_process');
8
+ const path = require('path');
9
+ const fs = require('fs');
10
+ const os = require('os');
11
+
12
+ const HOME = os.homedir();
13
+ const SYNC_SCRIPT = path.join(__dirname, '..', 'sync.sh');
14
+ const WRAPPER_MARKER = '# ─── skill-tags / Cursor Skill Command Sync';
15
+
16
+ module.exports = async function runSetup() {
17
+ const { select, confirm } = require('@inquirer/prompts');
18
+
19
+ console.log('\n skill-tags: setup\n');
20
+
21
+ const isGlobal = process.env.npm_config_global === 'true';
22
+
23
+ const mode = await select({
24
+ message: 'How are you using skill-tags?',
25
+ choices: [
26
+ {
27
+ name: 'Global — add skills() wrapper to shell profile (recommended for most users)',
28
+ value: 'global',
29
+ },
30
+ {
31
+ name: 'Project — add "skills" npm script to this project\'s package.json',
32
+ value: 'project',
33
+ },
34
+ ],
35
+ default: isGlobal ? 'global' : 'project',
36
+ });
37
+
38
+ const syncMode = await select({
39
+ message: 'How should skill-tags sync?',
40
+ choices: [
41
+ {
42
+ name: 'Auto (recommended) — sync automatically whenever you add or remove a skill',
43
+ value: 'auto',
44
+ },
45
+ {
46
+ name: 'Manual — run skill-tags yourself to sync when needed',
47
+ value: 'manual',
48
+ },
49
+ ],
50
+ default: 'auto',
51
+ });
52
+
53
+ if (mode === 'global') {
54
+ await setupGlobal(confirm, syncMode);
55
+ } else {
56
+ await setupProject(confirm, syncMode);
57
+ }
58
+ };
59
+
60
+ // ─── Global setup ─────────────────────────────────────────────────────────────
61
+
62
+ async function setupGlobal(confirm, syncMode) {
63
+ const shellName = path.basename(process.env.SHELL || 'bash');
64
+ let rcFile;
65
+ if (shellName === 'zsh') rcFile = path.join(HOME, '.zshrc');
66
+ else if (process.platform === 'darwin') rcFile = path.join(HOME, '.bash_profile');
67
+ else rcFile = path.join(HOME, '.bashrc');
68
+
69
+ const displayRc = rcFile.replace(HOME, '~');
70
+
71
+ if (syncMode === 'manual') {
72
+ console.log('\n Manual sync selected.');
73
+ console.log(' Run skill-tags at any time to regenerate ~/.cursor/commands/skill-tags.md\n');
74
+ console.log(' Running initial sync...\n');
75
+ spawnSync('bash', [SYNC_SCRIPT], { stdio: 'inherit' });
76
+ console.log(`\n Done! Run skill-tags manually to re-sync.\n`);
77
+ return;
78
+ }
79
+
80
+ // Auto sync: add shell wrapper
81
+ let alreadyInstalled = false;
82
+ try {
83
+ const content = fs.readFileSync(rcFile, 'utf-8');
84
+ if (content.includes(WRAPPER_MARKER)) alreadyInstalled = true;
85
+ } catch {}
86
+
87
+ if (alreadyInstalled) {
88
+ console.log(`\n ✓ Shell wrapper already installed in ${displayRc}\n`);
89
+ process.exit(0);
90
+ }
91
+
92
+ console.log(`\n This will add a skills() shell wrapper to ${displayRc}`);
93
+ console.log(` It auto-syncs skill-tags.md after every skills add/remove.\n`);
94
+
95
+ const yes = await confirm({
96
+ message: `Add the wrapper to ${displayRc}?`,
97
+ default: true,
98
+ });
99
+
100
+ if (!yes) {
101
+ console.log('\n Skipped.\n');
102
+ process.exit(0);
103
+ }
104
+
105
+ const syncPath = fs.existsSync(path.join(HOME, '.cursor', 'sync-skill-commands.sh'))
106
+ ? path.join(HOME, '.cursor', 'sync-skill-commands.sh')
107
+ : SYNC_SCRIPT;
108
+
109
+ const wrapper = `
110
+ ${WRAPPER_MARKER} ────────────────────────────────────────────────
111
+ # Wraps \`npx skills\` to auto-generate skill-tags.md after install/removal.
112
+ # Run manually: skill-tags (or: bash ${syncPath})
113
+ function skills() {
114
+ npx skills "$@"
115
+ local exit_code=$?
116
+ if [[ "$1" == "add" || "$1" == "remove" ]] && [[ $exit_code -eq 0 ]]; then
117
+ bash "${syncPath}"
118
+ fi
119
+ return $exit_code
120
+ }
121
+ # ─────────────────────────────────────────────────────────────────────────────
122
+ `;
123
+
124
+ try {
125
+ fs.appendFileSync(rcFile, wrapper);
126
+ } catch (err) {
127
+ console.error(`\n Failed to write to ${displayRc}: ${err.message}\n`);
128
+ process.exit(1);
129
+ }
130
+
131
+ console.log(`\n ✓ Added skills() wrapper to ${displayRc}`);
132
+ console.log(` Reload with: source ${displayRc}\n`);
133
+ }
134
+
135
+ // ─── Project setup ────────────────────────────────────────────────────────────
136
+
137
+ async function setupProject(confirm, syncMode) {
138
+ const pkgPath = path.join(process.cwd(), 'package.json');
139
+
140
+ if (!fs.existsSync(pkgPath)) {
141
+ console.error('\n No package.json found in current directory.');
142
+ console.error(' Run skill-tags --setup from your project root.\n');
143
+ process.exit(1);
144
+ }
145
+
146
+ let pkg;
147
+ try {
148
+ pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
149
+ } catch (err) {
150
+ console.error(`\n Failed to read package.json: ${err.message}\n`);
151
+ process.exit(1);
152
+ }
153
+
154
+ const scripts = pkg.scripts || {};
155
+
156
+ if (syncMode === 'manual') {
157
+ console.log('\n Manual sync selected.');
158
+ console.log(' Run skill-tags --local at any time to regenerate .cursor/commands/project-skill-tags.md\n');
159
+ console.log(' Running initial project sync...\n');
160
+ spawnSync(
161
+ 'node',
162
+ [path.join(__dirname, 'skill-tags.js'), '--local'],
163
+ { stdio: 'inherit', cwd: process.cwd() }
164
+ );
165
+ console.log(`\n Done! Run skill-tags --local manually to re-sync.\n`);
166
+ return;
167
+ }
168
+
169
+ // Auto sync: add "skills" npm script
170
+ if (scripts.skills === 'st-skills') {
171
+ console.log('\n ✓ Already configured — "skills": "st-skills" is in package.json\n');
172
+ console.log(' Add a skill: npm run skills add <owner/repo/skill-name>');
173
+ console.log(' Remove: npm run skills remove <owner/repo/skill-name>\n');
174
+ process.exit(0);
175
+ }
176
+
177
+ console.log('\n This will add a "skills" script to your package.json:');
178
+ console.log(' "scripts": { "skills": "st-skills" }');
179
+ console.log('\n Then use: npm run skills add <owner/repo/skill-name>');
180
+ console.log(' Auto-syncs .cursor/commands/project-skill-tags.md after every add/remove.\n');
181
+
182
+ const yes = await confirm({
183
+ message: 'Add the "skills" script to package.json?',
184
+ default: true,
185
+ });
186
+
187
+ if (!yes) {
188
+ console.log('\n Skipped.\n');
189
+ process.exit(0);
190
+ }
191
+
192
+ pkg.scripts = { ...scripts, skills: 'st-skills' };
193
+
194
+ try {
195
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
196
+ } catch (err) {
197
+ console.error(`\n Failed to write package.json: ${err.message}\n`);
198
+ process.exit(1);
199
+ }
200
+
201
+ console.log('\n ✓ Added "skills": "st-skills" to package.json');
202
+ console.log('\n Running initial project sync...\n');
203
+
204
+ const result = spawnSync(
205
+ 'node',
206
+ [path.join(__dirname, 'skill-tags.js'), '--local'],
207
+ { stdio: 'inherit', cwd: process.cwd() }
208
+ );
209
+
210
+ if (result.error) {
211
+ console.error(`\n Warning: initial sync failed — ${result.error.message}`);
212
+ }
213
+
214
+ console.log('\n Setup complete!\n');
215
+ console.log(' Add a skill: npm run skills add <owner/repo/skill-name>');
216
+ console.log(' Remove: npm run skills remove <owner/repo/skill-name>');
217
+ console.log(' Manual sync: skill-tags --local\n');
218
+ }
package/bin/skill-tags.js CHANGED
@@ -7,12 +7,9 @@
7
7
  const { spawnSync } = require('child_process');
8
8
  const path = require('path');
9
9
  const fs = require('fs');
10
- const os = require('os');
11
10
 
12
11
  const VERSION = require('../package.json').version;
13
- const HOME = os.homedir();
14
12
  const SYNC_SCRIPT = path.join(__dirname, '..', 'sync.sh');
15
- const WRAPPER_MARKER = '# ─── skill-tags / Cursor Skill Command Sync';
16
13
 
17
14
  // ─── --version ───────────────────────────────────────────────────────────────
18
15
 
@@ -40,9 +37,11 @@ if (process.argv.includes('--help') || process.argv.includes('-h')) {
40
37
  Options:
41
38
  (none) Scan all skill sources and generate skill-tags.md
42
39
  --categories Interactive category wizard (arrow keys + space to toggle)
43
- --global-only Skip project-level skills (.agents/skills in CWD)
44
- --project-only Scan only .agents/skills in CWD; write to .cursor/commands/skill-tags.md
45
- --setup Install the skills() shell wrapper in your shell rc
40
+ --categories --local Category wizard for project-level skills only
41
+ --global Skip local skills (.agents/skills in CWD); scan global sources only
42
+ --local Scan only .agents/skills in CWD; write to .cursor/commands/project-skill-tags.md
43
+ --setup Interactive setup: choose Global (shell profile) or Project (package.json)
44
+ --latest Update skill-tags to the latest version
46
45
  --version, -v Print version
47
46
  --help, -h Show this help
48
47
 
@@ -55,12 +54,23 @@ if (process.argv.includes('--help') || process.argv.includes('-h')) {
55
54
  ./.agents/skills/ project-level skills (current directory)
56
55
 
57
56
  Output:
58
- ~/.cursor/commands/skill-tags.md full index of all skills
59
- ~/.cursor/commands/skills-<category>.md generated by --categories
60
- ./.cursor/commands/skill-tags.md generated by --project-only
57
+ ~/.cursor/commands/skill-tags.md full index of all skills (global)
58
+ ~/.cursor/commands/skills-<category>.md generated by --categories
59
+ ./.cursor/commands/project-skill-tags.md generated by --local
60
+ ./.cursor/commands/skills-<category>.md generated by --categories --local
61
61
 
62
62
  Config:
63
- ~/.cursor/skill-tags-categories.conf
63
+ ~/.cursor/skill-tags-categories.conf global category config
64
+ ./.cursor/skill-tags-categories.conf project category config (--categories --local)
65
+
66
+ Setup modes:
67
+ Global Adds skills() wrapper to ~/.zshrc — use: skills add <pkg>
68
+ Project Adds "skills": "st-skills" to package.json — use: npm run skills add <pkg>
69
+
70
+ Project binary (st-skills):
71
+ st-skills add <pkg> Install a project skill and auto-sync
72
+ st-skills remove <pkg> Remove a project skill and auto-sync
73
+ st-skills update <pkg> Update a project skill and auto-sync
64
74
  `);
65
75
  process.exit(0);
66
76
  }
@@ -71,9 +81,68 @@ if (process.argv.includes('--categories')) {
71
81
  require('./categories');
72
82
  }
73
83
 
84
+ // ─── --latest ────────────────────────────────────────────────────────────────
85
+
86
+ else if (process.argv.includes('--latest')) {
87
+ const https = require('https');
88
+
89
+ function getLatestVersion() {
90
+ return new Promise((resolve, reject) => {
91
+ https.get('https://registry.npmjs.org/skill-tags/latest', { headers: { Accept: 'application/json' } }, res => {
92
+ let data = '';
93
+ res.on('data', chunk => { data += chunk; });
94
+ res.on('end', () => {
95
+ try { resolve(JSON.parse(data).version); }
96
+ catch { reject(new Error('Failed to parse registry response')); }
97
+ });
98
+ }).on('error', reject);
99
+ });
100
+ }
101
+
102
+ (async () => {
103
+ console.log(`\n skill-tags v${VERSION}\n`);
104
+
105
+ let latest;
106
+ try {
107
+ process.stdout.write(' Checking for updates...');
108
+ latest = await getLatestVersion();
109
+ process.stdout.write(` v${latest}\n`);
110
+ } catch (err) {
111
+ console.error(`\n Could not reach npm registry: ${err.message}\n`);
112
+ process.exit(1);
113
+ }
114
+
115
+ if (latest === VERSION) {
116
+ console.log(' Already on the latest version.\n');
117
+ process.exit(0);
118
+ }
119
+
120
+ console.log(` Updating: v${VERSION} → v${latest}\n`);
121
+ const result = spawnSync('npm', ['install', '-g', 'skill-tags@latest'], {
122
+ stdio: 'inherit',
123
+ shell: true,
124
+ });
125
+
126
+ if (result.error) {
127
+ console.error(`\n Update failed: ${result.error.message}`);
128
+ console.error(' Try manually: npm install -g skill-tags@latest\n');
129
+ process.exit(1);
130
+ }
131
+
132
+ if (result.status !== 0) {
133
+ console.error('\n Update failed. You may need to run with sudo:');
134
+ console.error(' sudo npm install -g skill-tags@latest\n');
135
+ process.exit(result.status);
136
+ }
137
+
138
+ console.log(`\n ✓ Updated to v${latest}\n`);
139
+ })();
140
+ }
141
+
74
142
  // ─── --setup ─────────────────────────────────────────────────────────────────
75
143
 
76
144
  else if (process.argv.includes('--setup')) {
145
+ const runSetup = require('./setup');
77
146
  runSetup().catch(err => {
78
147
  if (err.name === 'ExitPromptError') { console.log(); process.exit(0); }
79
148
  console.error(' Error:', err.message);
@@ -100,72 +169,3 @@ else {
100
169
 
101
170
  process.exit(result.status ?? 0);
102
171
  }
103
-
104
- // ─── Setup implementation ────────────────────────────────────────────────────
105
-
106
- async function runSetup() {
107
- const { confirm } = require('@inquirer/prompts');
108
-
109
- const shellName = path.basename(process.env.SHELL || 'bash');
110
- let rcFile;
111
- if (shellName === 'zsh') rcFile = path.join(HOME, '.zshrc');
112
- else if (process.platform === 'darwin') rcFile = path.join(HOME, '.bash_profile');
113
- else rcFile = path.join(HOME, '.bashrc');
114
-
115
- const displayRc = rcFile.replace(HOME, '~');
116
-
117
- console.log(`\n skill-tags: shell setup\n`);
118
-
119
- let alreadyInstalled = false;
120
- try {
121
- const content = fs.readFileSync(rcFile, 'utf-8');
122
- if (content.includes(WRAPPER_MARKER)) alreadyInstalled = true;
123
- } catch {}
124
-
125
- if (alreadyInstalled) {
126
- console.log(` ✓ Shell wrapper already installed in ${displayRc}\n`);
127
- process.exit(0);
128
- }
129
-
130
- console.log(` This will add a skills() shell wrapper to ${displayRc}`);
131
- console.log(` It auto-syncs skill-tags.md after every skills add/remove.\n`);
132
-
133
- const yes = await confirm({
134
- message: `Add the wrapper to ${displayRc}?`,
135
- default: true,
136
- });
137
-
138
- if (!yes) {
139
- console.log('\n Skipped.\n');
140
- process.exit(0);
141
- }
142
-
143
- const syncPath = fs.existsSync(path.join(HOME, '.cursor', 'sync-skill-commands.sh'))
144
- ? path.join(HOME, '.cursor', 'sync-skill-commands.sh')
145
- : SYNC_SCRIPT;
146
-
147
- const wrapper = `
148
- ${WRAPPER_MARKER} ────────────────────────────────────────────────
149
- # Wraps \`npx skills\` to auto-generate skill-tags.md after install/removal.
150
- # Run manually: skill-tags (or: bash ${syncPath})
151
- function skills() {
152
- npx skills "$@"
153
- local exit_code=$?
154
- if [[ "$1" == "add" || "$1" == "remove" ]] && [[ $exit_code -eq 0 ]]; then
155
- bash "${syncPath}"
156
- fi
157
- return $exit_code
158
- }
159
- # ─────────────────────────────────────────────────────────────────────────────
160
- `;
161
-
162
- try {
163
- fs.appendFileSync(rcFile, wrapper);
164
- } catch (err) {
165
- console.error(`\n Failed to write to ${displayRc}: ${err.message}\n`);
166
- process.exit(1);
167
- }
168
-
169
- console.log(`\n ✓ Added skills() wrapper to ${displayRc}`);
170
- console.log(` Reload with: source ${displayRc}\n`);
171
- }
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ // bin/st-skills.js
3
+ // Project-level skills wrapper: runs npx skills and auto-syncs project-skill-tags.md on add/remove/update.
4
+
5
+ 'use strict';
6
+
7
+ const { spawnSync } = require('child_process');
8
+ const path = require('path');
9
+
10
+ const args = process.argv.slice(2);
11
+ const subcommand = args[0];
12
+
13
+ // Run npx skills with all forwarded arguments
14
+ const result = spawnSync('npx', ['skills', ...args], { stdio: 'inherit', shell: false });
15
+
16
+ if (result.error) {
17
+ console.error(`\n st-skills: failed to run npx skills — ${result.error.message}\n`);
18
+ process.exit(1);
19
+ }
20
+
21
+ const exitCode = result.status ?? 1;
22
+
23
+ // Auto-sync project-skill-tags.md on successful mutating subcommands
24
+ if (['add', 'remove', 'update'].includes(subcommand) && exitCode === 0) {
25
+ const syncResult = spawnSync(
26
+ 'node',
27
+ [path.join(__dirname, 'skill-tags.js'), '--local'],
28
+ { stdio: 'inherit', cwd: process.cwd() }
29
+ );
30
+
31
+ if (syncResult.error) {
32
+ console.error(`\n st-skills: sync failed — ${syncResult.error.message}\n`);
33
+ }
34
+ }
35
+
36
+ process.exit(exitCode);
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "skill-tags",
3
- "version": "1.2.1",
3
+ "version": "1.5.0",
4
4
  "description": "Generates consolidated command files (@skill-tags.md and custom @skills-<category>.md indexes) for all installed Cursor skills.",
5
5
  "bin": {
6
- "skill-tags": "bin/skill-tags.js"
6
+ "skill-tags": "bin/skill-tags.js",
7
+ "st-skills": "bin/st-skills.js"
7
8
  },
8
9
  "scripts": {
9
10
  "postinstall": "node ./bin/postinstall.js"
package/sync.sh CHANGED
@@ -7,13 +7,13 @@
7
7
 
8
8
  set -euo pipefail
9
9
 
10
- VERSION="1.2.1"
10
+ VERSION="1.5.0"
11
11
 
12
12
  GLOBAL_COMMANDS_DIR="${HOME}/.cursor/commands"
13
13
  OUTPUT_FILE="${GLOBAL_COMMANDS_DIR}/skill-tags.md"
14
14
  CATEGORIES_CONFIG="${HOME}/.cursor/skill-tags-categories.conf"
15
15
  PROJECT_COMMANDS_DIR="$(pwd)/.cursor/commands"
16
- PROJECT_OUTPUT_FILE="${PROJECT_COMMANDS_DIR}/skill-tags.md"
16
+ PROJECT_OUTPUT_FILE="${PROJECT_COMMANDS_DIR}/project-skill-tags.md"
17
17
 
18
18
  # ─── Priority-ordered skill source directories ─────────────────────────────────
19
19
  # Earlier entries take priority when the same skill name exists in multiple locations.
@@ -36,11 +36,13 @@ trap 'rm -f "$SKILLS_TEMP"; rm -rf "$SKILLS_META_DIR"' EXIT
36
36
 
37
37
  GLOBAL_ONLY=false
38
38
  PROJECT_ONLY=false
39
+ QUIET=false
39
40
 
40
41
  for arg in "$@"; do
41
42
  case "$arg" in
42
- --global-only) GLOBAL_ONLY=true ;;
43
- --project-only) PROJECT_ONLY=true ;;
43
+ --global) GLOBAL_ONLY=true ;;
44
+ --local) PROJECT_ONLY=true ;;
45
+ --quiet) QUIET=true ;;
44
46
  esac
45
47
  done
46
48
 
@@ -49,8 +51,8 @@ done
49
51
  count_found=0
50
52
  count_dupes=0
51
53
 
52
- log() { printf " %s\n" "$*"; }
53
- success() { printf " ✓ %s\n" "$*"; }
54
+ log() { [[ "$QUIET" == "true" ]] || printf " %s\n" "$*"; }
55
+ success() { [[ "$QUIET" == "true" ]] || printf " ✓ %s\n" "$*"; }
54
56
 
55
57
  # Tracks which skill names have already been processed (deduplication).
56
58
  # Uses a delimited string for bash 3.2 compatibility (no associative arrays).
@@ -192,10 +194,14 @@ scan_tree() {
192
194
 
193
195
  # ─── Generate category files ────────────────────────────────────────────────────
194
196
 
195
- # Reads ~/.cursor/skill-tags-categories.conf and writes a skills-<category>.md
196
- # command file for each category. Called automatically on every sync when config exists.
197
+ # Reads a categories config and writes a skills-<category>.md command file for each.
198
+ # Usage: generate_category_files [config_file] [commands_dir]
199
+ # Defaults to global paths when called without arguments.
197
200
  generate_category_files() {
198
- [[ -f "$CATEGORIES_CONFIG" ]] || return 0
201
+ local config_file="${1:-$CATEGORIES_CONFIG}"
202
+ local commands_dir="${2:-$GLOBAL_COMMANDS_DIR}"
203
+
204
+ [[ -f "$config_file" ]] || return 0
199
205
 
200
206
  local gen_count=0
201
207
 
@@ -205,7 +211,7 @@ generate_category_files() {
205
211
 
206
212
  local title
207
213
  title="$(to_title_case "$cat_name")"
208
- local out="${GLOBAL_COMMANDS_DIR}/skills-${cat_name}.md"
214
+ local out="${commands_dir}/skills-${cat_name}.md"
209
215
 
210
216
  local skills_section=""
211
217
  while IFS= read -r sname; do
@@ -247,19 +253,21 @@ EOF
247
253
 
248
254
  gen_count=$(( gen_count + 1 ))
249
255
  success "Generated: ${out/#$HOME/~}"
250
- done < "$CATEGORIES_CONFIG"
256
+ done < "$config_file"
251
257
 
252
- if [[ $gen_count -gt 0 ]]; then
258
+ if [[ $gen_count -gt 0 && "$QUIET" != "true" ]]; then
253
259
  printf " Category files: %d generated\n" "$gen_count"
254
260
  fi
255
261
  }
256
262
 
257
263
  # ─── Main ──────────────────────────────────────────────────────────────────────
258
264
 
259
- printf "\n skill-tags v%s syncing skills\n\n" "$VERSION"
265
+ if [[ "$QUIET" != "true" ]]; then
266
+ printf "\n skill-tags v%s — syncing skills\n\n" "$VERSION"
267
+ fi
260
268
 
261
269
  if [[ "$PROJECT_ONLY" == "true" ]]; then
262
- # ─── --project-only: scan only .agents/skills in CWD ────────────────────────
270
+ # ─── --local: scan only .agents/skills in CWD ───────────────────────────────
263
271
  PROJECT_SKILLS_DIR="$(pwd)/.agents/skills"
264
272
 
265
273
  if [[ ! -d "$PROJECT_SKILLS_DIR" ]]; then
@@ -297,14 +305,19 @@ ${OPENING}
297
305
  $(cat "$SKILLS_TEMP")
298
306
  EOF
299
307
 
300
- printf "\n"
301
- if [[ "$is_update" == "true" ]]; then
302
- printf " ✓ Updated: .cursor/commands/skill-tags.md\n"
303
- else
304
- printf " ✓ Generated: .cursor/commands/skill-tags.md\n"
308
+ PROJECT_CATEGORIES_CONFIG="$(pwd)/.cursor/skill-tags-categories.conf"
309
+ generate_category_files "$PROJECT_CATEGORIES_CONFIG" "$PROJECT_COMMANDS_DIR"
310
+
311
+ if [[ "$QUIET" != "true" ]]; then
312
+ printf "\n"
313
+ if [[ "$is_update" == "true" ]]; then
314
+ printf " ✓ Updated: .cursor/commands/project-skill-tags.md\n"
315
+ else
316
+ printf " ✓ Generated: .cursor/commands/project-skill-tags.md\n"
317
+ fi
318
+ printf " Skills: %d indexed\n" "$count_found"
319
+ printf "\n Tip: type @project-skill-tags.md in Cursor chat to load the project skills reference.\n\n"
305
320
  fi
306
- printf " Skills: %d indexed\n" "$count_found"
307
- printf "\n Tip: type @skill-tags.md in Cursor chat to load the project skills reference.\n\n"
308
321
 
309
322
  else
310
323
  # ─── Default: scan all sources ───────────────────────────────────────────────
@@ -352,16 +365,18 @@ EOF
352
365
 
353
366
  generate_category_files
354
367
 
355
- printf "\n"
356
- if [[ "$is_update" == "true" ]]; then
357
- printf " ✓ Updated: %s\n" "${OUTPUT_FILE/#$HOME/~}"
358
- else
359
- printf " ✓ Generated: %s\n" "${OUTPUT_FILE/#$HOME/~}"
360
- fi
361
- printf " Skills: %d indexed\n" "$count_found"
362
- if [[ $count_dupes -gt 0 ]]; then
363
- printf " Duplicates: %d skipped (covered by higher-priority source)\n" "$count_dupes"
368
+ if [[ "$QUIET" != "true" ]]; then
369
+ printf "\n"
370
+ if [[ "$is_update" == "true" ]]; then
371
+ printf " ✓ Updated: %s\n" "${OUTPUT_FILE/#$HOME/~}"
372
+ else
373
+ printf " ✓ Generated: %s\n" "${OUTPUT_FILE/#$HOME/~}"
374
+ fi
375
+ printf " Skills: %d indexed\n" "$count_found"
376
+ if [[ $count_dupes -gt 0 ]]; then
377
+ printf " Duplicates: %d skipped (covered by higher-priority source)\n" "$count_dupes"
378
+ fi
379
+ printf "\n Tip: type @skill-tags.md in Cursor chat to load the full skills reference.\n\n"
364
380
  fi
365
- printf "\n Tip: type @skill-tags.md in Cursor chat to load the full skills reference.\n\n"
366
381
 
367
382
  fi