skill-tags 1.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Steven Light
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,256 @@
1
+ # skill-tags
2
+
3
+ Generate a single consolidated Cursor command file for all installed skills, making it easy to reference your entire skill library using `@skill-tags.md` in Cursor chat.
4
+
5
+ > [!IMPORTANT]
6
+ > Cursor's Agent Skills feature only loads skill frontmatter into the agent's context window. Skills are "intelligently applied", but I have had little to no success with them unless I explicitly ask the model to apply the skill.
7
+
8
+ skill-tags bypasses this by generating a single command file (`~/.cursor/commands/skill-tags.md`) that lists all available skills, their paths, and their descriptions. By referencing this single file, you give the agent a complete index of all skills on your system and instruct it to autonomously choose the best skill(s) for the current task.
9
+
10
+ > [!TIP]
11
+ > Stay tuned for a more permanent solution to this problem: I'm developing an open source community called [Cursor Kits](https://cursorkits.com). Cursor Kits is something I started before Cursor launched their [Plugin Marketplace](https://cursor.com/marketplace). It's the same idea, but built for the community (vs integration providers).
12
+
13
+ If you're interested in contributing to Cursor Kits, please let me know!
14
+
15
+ ---
16
+
17
+ ## Table of Contents
18
+
19
+ - [Quick Start](#quick-start)
20
+ - [Usage](#usage)
21
+ - [Categorized Indexes](#categorized-indexes)
22
+ - [Agent Setup Prompt](#agent-setup-prompt)
23
+ - [Install Options](#install-options)
24
+ - [How It Works](#how-it-works)
25
+ - [CLI Reference](#cli-reference)
26
+ - [Manual Sync](#manual-sync)
27
+ - [Skill Sources Scanned](#skill-sources-scanned)
28
+ - [Generated File Format](#generated-file-format)
29
+ - [Skill Metadata Tags](#skill-metadata-tags)
30
+ - [Uninstall](#uninstall)
31
+ - [Requirements](#requirements)
32
+ - [License](#license)
33
+
34
+ ---
35
+
36
+ ## Quick Start
37
+
38
+ ```bash
39
+ # Install (global, recommended)
40
+ npm install -g skill-tags
41
+
42
+ # Add the shell auto-trigger wrapper
43
+ skill-tags --setup
44
+ source ~/.zshrc
45
+
46
+ # Initial sync (generate command file)
47
+ skill-tags
48
+ ```
49
+
50
+ ## Usage
51
+
52
+ In any Cursor chat, attach the full index of your skills by referencing the generated command file:
53
+
54
+ - `@skill-tags.md`
55
+
56
+ Example:
57
+
58
+ ```text
59
+ @skill-tags.md Help me refactor this React component to be more performant.
60
+ ```
61
+
62
+ The agent will assess the available skills listed in the file, automatically determine which ones are relevant (e.g., `vercel-react-best-practices`), and apply them to your request. If the agent is in **Plan Mode**, it will also proactively reference specific skills to be used in the generated plan and TODOs.
63
+
64
+ ## Categorized Indexes
65
+
66
+ For more focused context windows, you can group your skills into category-specific index files using the interactive wizard:
67
+
68
+ ```bash
69
+ skill-tags --categories
70
+ ```
71
+
72
+ This opens a CRUD wizard where you can create, edit, and delete categories. Skill assignment is powered by a two-tier auto-suggestion system:
73
+
74
+ 1. **`metadata.tags` (high confidence)** — if a skill's `SKILL.md` includes a `metadata.tags` frontmatter field, those tags are matched directly against the category.
75
+ 2. **Keyword fallback** — for skills without `metadata.tags`, the skill name and description are scanned against a built-in keyword map.
76
+
77
+ Suggested skills are pre-selected (`[*]`). You can toggle any skill in or out by number before confirming.
78
+
79
+ Once configured, every `skill-tags` sync automatically regenerates the category files from the saved config at `~/.cursor/skill-tags-categories.conf`.
80
+
81
+ ```bash
82
+ # Inject a focused category index instead of the full list:
83
+ @skills-frontend.md
84
+ @skills-testing.md
85
+ @skills-ai-agents.md
86
+ ```
87
+
88
+ Predefined categories: `frontend`, `backend`, `database`, `testing`, `accessibility`, `performance`, `ai-agents`, `devops`, `design`. You can also create custom categories with any name.
89
+
90
+ ---
91
+
92
+ ## Agent Setup Prompt
93
+
94
+ Copy and paste this into your Cursor agent to autoconfigure skill-tags:
95
+
96
+ <details>
97
+ <summary>Click to expand the full setup prompt</summary>
98
+
99
+ ```text
100
+ Install and configure the skill-tags package for me.
101
+
102
+ First, confirm with me: should this be a global install (adds `skill-tags` to PATH, recommended for most users) or a local project devDependency? Wait for my answer before proceeding.
103
+
104
+ Once confirmed, 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).
105
+
106
+ Steps to perform:
107
+ 1. Install the package based on my preference:
108
+ - Global: `npm install -g skill-tags`
109
+ - Local: `npm install --save-dev skill-tags`
110
+ 2. Run `skill-tags --setup` to install the `skills()` shell wrapper in my rc file (~/.zshrc or ~/.bash_profile)
111
+ 3. Run `skill-tags` to perform an initial sync of all installed skills
112
+ 4. Source my shell rc file or instruct me to do so manually
113
+
114
+ When complete, output a summary that includes:
115
+ - Where the command file was generated (~/.cursor/commands/skill-tags.md)
116
+ - How to use it: typing @skill-tags.md in any Cursor chat attaches the full index of skills for the agent to assess
117
+ - How the auto-trigger works: `skills add/remove <pkg>` now automatically syncs after every install/removal
118
+ - How to manually re-sync at any time: run `skill-tags`
119
+ - The total number of skills that were indexed
120
+ ```
121
+
122
+ </details>
123
+
124
+ ---
125
+
126
+ ## Install Options
127
+
128
+ ### Install via npm
129
+
130
+ ```bash
131
+ # Global install (recommended) — adds `skill-tags` to your PATH
132
+ npm install -g skill-tags
133
+
134
+ # One-off run without installing
135
+ npx skill-tags
136
+
137
+ # Project devDependency (adds to package.json)
138
+ npm install --save-dev skill-tags
139
+ ```
140
+
141
+ After global install, set up the shell auto-trigger wrapper:
142
+
143
+ ```bash
144
+ skill-tags --setup
145
+ source ~/.zshrc
146
+ ```
147
+
148
+ ### Install via curl
149
+
150
+ ```bash
151
+ curl -fsSL https://raw.githubusercontent.com/steve-piece/skill-tags/main/install.sh | bash
152
+ ```
153
+
154
+ Then reload your shell:
155
+
156
+ ```bash
157
+ source ~/.zshrc # or ~/.bash_profile / ~/.bashrc
158
+ ```
159
+
160
+ ## How It Works
161
+
162
+ After setup, the `skills` command wraps `npx skills` and automatically runs a sync after every `skills add` or `skills remove`:
163
+
164
+ ```bash
165
+ # Install (or remove) a skill — sync runs automatically
166
+ skills add vercel-labs/agent-skills/vercel-react-best-practices
167
+
168
+ # The single command file is updated:
169
+ # ~/.cursor/commands/skill-tags.md
170
+
171
+ # Use it in Cursor chat:
172
+ # @skill-tags.md
173
+ ```
174
+
175
+ ## CLI Reference
176
+
177
+ ```bash
178
+ skill-tags # sync all skills, generate/update the command file
179
+ skill-tags --categories # open interactive category wizard (CRUD)
180
+ skill-tags --setup # install skills() shell wrapper in ~/.zshrc
181
+ skill-tags --global-only # skip project-level skills
182
+ skill-tags --version # print version
183
+ skill-tags --help # show usage
184
+ ```
185
+
186
+ ## Manual Sync
187
+
188
+ Run at any time to regenerate the command file:
189
+
190
+ ```bash
191
+ skill-tags
192
+
193
+ # Or via bash directly:
194
+ bash ~/.cursor/sync-skill-commands.sh
195
+ ```
196
+
197
+ ## Skill Sources Scanned
198
+
199
+ Skills are discovered from all of these locations automatically. When the same skill name appears in multiple sources, the first match wins (priority order):
200
+
201
+ | Priority | Directory | Source |
202
+ |---|---|---|
203
+ | 1 | `~/.agents/skills/` | `npx skills add` installs |
204
+ | 2 | `~/.cursor/skills-cursor/` | Cursor built-in skills |
205
+ | 3 | `~/.cursor/plugins/cache/` | Cursor Marketplace plugins |
206
+ | 4 | `~/.claude/plugins/cache/` | Claude plugins |
207
+ | 5 | `~/.codex/skills/` | Codex skills |
208
+ | 6 | `./.agents/skills/` | Project-level skills (CWD) |
209
+
210
+ ## Generated File Format
211
+
212
+ The `~/.cursor/commands/skill-tags.md` contains:
213
+
214
+ - An opening instruction for the agent to assess all skills and apply them autonomously.
215
+ - Instructions for the agent on how to plan with skills when in Plan Mode.
216
+ - A full list of available skills, including their titles, directory paths, and descriptions.
217
+
218
+ This gives the agent a complete map of your skill library when you reference it with `@`.
219
+
220
+ ## Skill Metadata Tags
221
+
222
+ skill-tags uses the official `metadata` frontmatter field from the [skills.sh](https://skills.sh) spec to improve auto-categorization. If you are authoring a skill, add `metadata.tags` to improve how it is categorized:
223
+
224
+ ```yaml
225
+ ---
226
+ name: my-skill
227
+ description: Does X, Y, Z.
228
+ metadata:
229
+ tags: [frontend, react, animation]
230
+ ---
231
+ ```
232
+
233
+ Skills with `metadata.tags` are surfaced as high-confidence matches in the `--categories` wizard and marked `[*]` with the tag source shown inline. Skills without tags fall back to keyword matching.
234
+
235
+ ---
236
+
237
+ ## Uninstall
238
+
239
+ ```bash
240
+ # Via npm
241
+ npm uninstall -g skill-tags
242
+
243
+ # Clean up generated command file and shell wrapper
244
+ bash uninstall.sh
245
+ ```
246
+
247
+ ## Requirements
248
+
249
+ - macOS or Linux (Windows not supported — requires bash)
250
+ - Node.js >=14 (for npm install)
251
+ - bash or zsh
252
+ - [Cursor IDE](https://cursor.com)
253
+
254
+ ## License
255
+
256
+ MIT
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+ // bin/postinstall.js
3
+ // Runs automatically after `npm install -g skill-tags`.
4
+ // Performs an initial sync of any already-installed skills.
5
+ // Never throws — a failed postinstall should never break npm install.
6
+
7
+ 'use strict';
8
+
9
+ const { spawnSync } = require('child_process');
10
+ const path = require('path');
11
+ const fs = require('fs');
12
+
13
+ // Skip entirely on Windows
14
+ if (process.platform === 'win32') {
15
+ process.exit(0);
16
+ }
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
+ }
23
+
24
+ const syncScript = path.join(__dirname, '..', 'sync.sh');
25
+
26
+ if (!fs.existsSync(syncScript)) {
27
+ process.exit(0);
28
+ }
29
+
30
+ console.log('\n skill-tags: running initial sync...\n');
31
+
32
+ try {
33
+ const result = spawnSync('bash', [syncScript], { stdio: 'inherit' });
34
+ if (result.error) throw result.error;
35
+ } catch (_) {
36
+ // Never fail the install
37
+ }
38
+
39
+ console.log(' To auto-sync after every `skills add`, run:');
40
+ console.log(' skill-tags --setup\n');
41
+
42
+ process.exit(0);
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ // bin/skill-tags.js
3
+ // CLI entry point for skill-tags. Spawns sync.sh with bash and passes all args through.
4
+
5
+ 'use strict';
6
+
7
+ const { spawnSync } = require('child_process');
8
+ const path = require('path');
9
+ const fs = require('fs');
10
+
11
+ const VERSION = require('../package.json').version;
12
+
13
+ // Handle --version before delegating to bash
14
+ if (process.argv.includes('--version') || process.argv.includes('-v')) {
15
+ console.log(`skill-tags v${VERSION}`);
16
+ process.exit(0);
17
+ }
18
+
19
+ // Windows is not supported (requires bash)
20
+ if (process.platform === 'win32') {
21
+ console.error('skill-tags requires bash and is not supported on Windows.');
22
+ console.error('Consider using WSL (Windows Subsystem for Linux) instead.');
23
+ process.exit(1);
24
+ }
25
+
26
+ // Locate sync.sh bundled alongside this package
27
+ const syncScript = path.join(__dirname, '..', 'sync.sh');
28
+
29
+ if (!fs.existsSync(syncScript)) {
30
+ console.error(`skill-tags: sync.sh not found at ${syncScript}`);
31
+ console.error('Try reinstalling: npm install -g skill-tags');
32
+ process.exit(1);
33
+ }
34
+
35
+ // Pass all CLI arguments through to sync.sh
36
+ const args = process.argv.slice(2);
37
+ const result = spawnSync('bash', [syncScript, ...args], { stdio: 'inherit' });
38
+
39
+ if (result.error) {
40
+ console.error(`skill-tags: failed to run bash — ${result.error.message}`);
41
+ process.exit(1);
42
+ }
43
+
44
+ process.exit(result.status ?? 0);
package/install.sh ADDED
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env bash
2
+ # install.sh
3
+ # One-line installer for skill-tags.
4
+ #
5
+ # Usage (curl):
6
+ # curl -fsSL https://raw.githubusercontent.com/steve-piece/skill-tags/main/install.sh | bash
7
+ #
8
+ # Usage (local):
9
+ # bash install.sh
10
+
11
+ set -euo pipefail
12
+
13
+ REPO="https://raw.githubusercontent.com/steve-piece/skill-tags/main"
14
+ SYNC_SCRIPT_DEST="${HOME}/.cursor/sync-skill-commands.sh"
15
+ CURSOR_COMMANDS_DIR="${HOME}/.cursor/commands"
16
+ WRAPPER_MARKER="# ─── skill-tags / Cursor Skill Command Sync"
17
+
18
+ # ─── Helpers ───────────────────────────────────────────────────────────────────
19
+
20
+ info() { printf " %s\n" "$*"; }
21
+ success() { printf " ✓ %s\n" "$*"; }
22
+ warn() { printf " ⚠ %s\n" "$*"; }
23
+ die() { printf "\n ✗ Error: %s\n\n" "$*" >&2; exit 1; }
24
+
25
+ # ─── Shell detection ───────────────────────────────────────────────────────────
26
+
27
+ detect_rc() {
28
+ local shell_name
29
+ shell_name="$(basename "${SHELL:-bash}")"
30
+ if [[ "$shell_name" == "zsh" ]]; then
31
+ echo "${HOME}/.zshrc"
32
+ elif [[ "$shell_name" == "bash" ]]; then
33
+ # macOS uses ~/.bash_profile, Linux uses ~/.bashrc
34
+ if [[ "$(uname)" == "Darwin" ]]; then
35
+ echo "${HOME}/.bash_profile"
36
+ else
37
+ echo "${HOME}/.bashrc"
38
+ fi
39
+ else
40
+ echo "${HOME}/.profile"
41
+ fi
42
+ }
43
+
44
+ # ─── Download or copy sync.sh ──────────────────────────────────────────────────
45
+
46
+ install_sync_script() {
47
+ mkdir -p "$(dirname "$SYNC_SCRIPT_DEST")"
48
+ mkdir -p "$CURSOR_COMMANDS_DIR"
49
+
50
+ # If running from a local clone, copy directly
51
+ local script_dir
52
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" 2>/dev/null && pwd || echo "")"
53
+
54
+ if [[ -n "$script_dir" && -f "${script_dir}/sync.sh" ]]; then
55
+ cp "${script_dir}/sync.sh" "$SYNC_SCRIPT_DEST"
56
+ info "Installed from local copy."
57
+ else
58
+ # Download from GitHub
59
+ if command -v curl &>/dev/null; then
60
+ curl -fsSL "${REPO}/sync.sh" -o "$SYNC_SCRIPT_DEST"
61
+ elif command -v wget &>/dev/null; then
62
+ wget -qO "$SYNC_SCRIPT_DEST" "${REPO}/sync.sh"
63
+ else
64
+ die "Neither curl nor wget found. Please install one and retry."
65
+ fi
66
+ info "Downloaded sync.sh from GitHub."
67
+ fi
68
+
69
+ chmod +x "$SYNC_SCRIPT_DEST"
70
+ success "Installed: ${SYNC_SCRIPT_DEST/#$HOME/~}"
71
+ }
72
+
73
+ # ─── Shell wrapper ─────────────────────────────────────────────────────────────
74
+
75
+ install_wrapper() {
76
+ local rc_file="$1"
77
+
78
+ # Idempotent: skip if already installed
79
+ if grep -q "$WRAPPER_MARKER" "$rc_file" 2>/dev/null; then
80
+ warn "Shell wrapper already present in ${rc_file/#$HOME/~} — skipping."
81
+ return 0
82
+ fi
83
+
84
+ # Create rc file if it doesn't exist
85
+ touch "$rc_file"
86
+
87
+ cat >> "$rc_file" <<'WRAPPER'
88
+
89
+ # ─── skill-tags / Cursor Skill Command Sync ────────────────────────────────────
90
+ # Wraps `npx skills` to auto-generate @skill-name.md command files after install.
91
+ # Run manually: skill-tags (or: bash ~/.cursor/sync-skill-commands.sh)
92
+ function skills() {
93
+ npx skills "$@"
94
+ local exit_code=$?
95
+ if [[ "$1" == "add" && $exit_code -eq 0 ]]; then
96
+ bash ~/.cursor/sync-skill-commands.sh
97
+ fi
98
+ return $exit_code
99
+ }
100
+ # ─────────────────────────────────────────────────────────────────────────────
101
+ WRAPPER
102
+
103
+ success "Added skills wrapper to ${rc_file/#$HOME/~}"
104
+ }
105
+
106
+ # ─── Main ──────────────────────────────────────────────────────────────────────
107
+
108
+ printf "\n🔧 Installing skill-tags...\n\n"
109
+
110
+ # 1. Install sync script
111
+ install_sync_script
112
+
113
+ # 2. Detect shell and install wrapper
114
+ RC_FILE="$(detect_rc)"
115
+ info "Detected shell rc: ${RC_FILE/#$HOME/~}"
116
+ install_wrapper "$RC_FILE"
117
+
118
+ # 3. Run initial sync
119
+ printf "\n Running initial sync...\n"
120
+ bash "$SYNC_SCRIPT_DEST" || true
121
+
122
+ # ─── Done ──────────────────────────────────────────────────────────────────────
123
+
124
+ printf " Installation complete!\n\n"
125
+ printf " Next steps:\n"
126
+ printf " 1. Reload your shell: source %s\n" "${RC_FILE/#$HOME/~}"
127
+ printf " 2. Install a skill: skills add <owner/repo/skill-name>\n"
128
+ printf " 3. Use in Cursor chat: @<skill-name>.md\n\n"
129
+ printf " To sync manually at any time:\n"
130
+ printf " bash ~/.cursor/sync-skill-commands.sh\n\n"
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "skill-tags",
3
+ "version": "1.1.0",
4
+ "description": "Generate a Cursor /skill-tags command file indexing all installed agent skills for autonomous context injection",
5
+ "bin": {
6
+ "skill-tags": "bin/skill-tags.js"
7
+ },
8
+ "scripts": {
9
+ "postinstall": "node ./bin/postinstall.js"
10
+ },
11
+ "engines": {
12
+ "node": ">=14"
13
+ },
14
+ "keywords": [
15
+ "cursor",
16
+ "cursor-ide",
17
+ "skills",
18
+ "agent-skills",
19
+ "skills.sh",
20
+ "skill-tags",
21
+ "ai",
22
+ "llm",
23
+ "mcp",
24
+ "claude",
25
+ "codex"
26
+ ],
27
+ "author": "Steven Light",
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/steve-piece/skill-tags.git"
32
+ },
33
+ "homepage": "https://github.com/steve-piece/skill-tags#readme",
34
+ "files": [
35
+ "bin/",
36
+ "sync.sh",
37
+ "install.sh",
38
+ "uninstall.sh",
39
+ "README.md",
40
+ "LICENSE"
41
+ ]
42
+ }
package/sync.sh ADDED
@@ -0,0 +1,773 @@
1
+ #!/usr/bin/env bash
2
+ # sync.sh
3
+ # Generates ~/.cursor/commands/skill-tags.md listing all installed skills.
4
+ # Optionally generates categorized skill files via --categories wizard.
5
+ # Scans every known skill location, deduplicates by name (first-found wins).
6
+ # Works on macOS and Linux. bash 3.2 compatible (macOS default).
7
+ #
8
+ # Usage:
9
+ # bash sync.sh # generate skill-tags.md
10
+ # bash sync.sh --categories # interactive category wizard (CRUD)
11
+ # bash sync.sh --global-only # skip project-level skills
12
+ # bash sync.sh --setup # install shell wrapper (skills() auto-trigger)
13
+ # bash sync.sh --version # print version
14
+ # bash sync.sh --help # show usage
15
+
16
+ set -euo pipefail
17
+
18
+ VERSION="1.1.0"
19
+
20
+ GLOBAL_COMMANDS_DIR="${HOME}/.cursor/commands"
21
+ OUTPUT_FILE="${GLOBAL_COMMANDS_DIR}/skill-tags.md"
22
+ CATEGORIES_CONFIG="${HOME}/.cursor/skill-tags-categories.conf"
23
+ WRAPPER_MARKER="# ─── skill-tags / Cursor Skill Command Sync"
24
+
25
+ # ─── Priority-ordered skill source directories ─────────────────────────────────
26
+ # Earlier entries take priority when the same skill name exists in multiple locations.
27
+ # Format: "path:label"
28
+ GLOBAL_SKILL_SOURCES=(
29
+ "${HOME}/.agents/skills:global skills"
30
+ "${HOME}/.cursor/skills-cursor:cursor built-in skills"
31
+ "${HOME}/.cursor/plugins/cache:cursor plugin cache"
32
+ "${HOME}/.claude/plugins/cache:claude plugin cache"
33
+ "${HOME}/.codex/skills:codex skills"
34
+ )
35
+
36
+ # ─── Temp files (created early; trap cleans up on any exit) ────────────────────
37
+
38
+ SKILLS_TEMP="$(mktemp)"
39
+ SKILLS_META_DIR="$(mktemp -d)"
40
+ trap 'rm -f "$SKILLS_TEMP"; rm -rf "$SKILLS_META_DIR"' EXIT
41
+
42
+ # ─── Shell setup helper ────────────────────────────────────────────────────────
43
+
44
+ detect_rc() {
45
+ local shell_name
46
+ shell_name="$(basename "${SHELL:-bash}")"
47
+ if [[ "$shell_name" == "zsh" ]]; then
48
+ echo "${HOME}/.zshrc"
49
+ elif [[ "$(uname)" == "Darwin" ]]; then
50
+ echo "${HOME}/.bash_profile"
51
+ else
52
+ echo "${HOME}/.bashrc"
53
+ fi
54
+ }
55
+
56
+ cmd_setup() {
57
+ local rc_file
58
+ rc_file="$(detect_rc)"
59
+
60
+ printf "\n skill-tags: shell setup\n\n"
61
+
62
+ if grep -q "$WRAPPER_MARKER" "$rc_file" 2>/dev/null; then
63
+ printf " Shell wrapper already installed in %s\n\n" "${rc_file/#$HOME/~}"
64
+ return 0
65
+ fi
66
+
67
+ local sync_path="${HOME}/.cursor/sync-skill-commands.sh"
68
+ if [[ ! -f "$sync_path" ]]; then
69
+ sync_path="$(cd "$(dirname "$0")" && pwd)/sync.sh"
70
+ fi
71
+
72
+ touch "$rc_file"
73
+ cat >> "$rc_file" <<WRAPPER
74
+
75
+ ${WRAPPER_MARKER} ────────────────────────────────────────────────
76
+ # Wraps \`npx skills\` to auto-generate skill-tags.md after install/removal.
77
+ # Run manually: skill-tags (or: bash ${sync_path})
78
+ function skills() {
79
+ npx skills "\$@"
80
+ local exit_code=\$?
81
+ if [[ "\$1" == "add" || "\$1" == "remove" ]] && [[ \$exit_code -eq 0 ]]; then
82
+ bash "${sync_path}"
83
+ fi
84
+ return \$exit_code
85
+ }
86
+ # ─────────────────────────────────────────────────────────────────────────────
87
+ WRAPPER
88
+
89
+ printf " Added skills() wrapper to %s\n" "${rc_file/#$HOME/~}"
90
+ printf " Reload with: source %s\n\n" "${rc_file/#$HOME/~}"
91
+ }
92
+
93
+ # ─── Flags ─────────────────────────────────────────────────────────────────────
94
+
95
+ GLOBAL_ONLY=false
96
+ RUN_CATEGORIES=false
97
+
98
+ for arg in "$@"; do
99
+ case "$arg" in
100
+ --global-only) GLOBAL_ONLY=true ;;
101
+ --categories) RUN_CATEGORIES=true ;;
102
+ --version|-v)
103
+ echo "skill-tags v${VERSION}"
104
+ exit 0
105
+ ;;
106
+ --setup)
107
+ cmd_setup
108
+ exit 0
109
+ ;;
110
+ --help|-h)
111
+ echo "skill-tags v${VERSION} — Cursor Skill Command Sync"
112
+ echo ""
113
+ echo "Usage: skill-tags [options]"
114
+ echo ""
115
+ echo "Options:"
116
+ echo " (none) Scan all skill sources and generate skill-tags.md"
117
+ echo " --categories Open interactive category wizard (create/edit/delete groups)"
118
+ echo " --global-only Skip project-level skills (.agents/skills in CWD)"
119
+ echo " --setup Install the skills() shell wrapper in ~/.zshrc (auto-trigger)"
120
+ echo " --version, -v Print version"
121
+ echo " --help, -h Show this help"
122
+ echo ""
123
+ echo "Skill sources scanned (priority order — first match wins):"
124
+ echo " ~/.agents/skills/ (skills installed via npx skills add)"
125
+ echo " ~/.cursor/skills-cursor/ (Cursor built-in skills)"
126
+ echo " ~/.cursor/plugins/cache/ (Cursor Marketplace plugin skills)"
127
+ echo " ~/.claude/plugins/cache/ (Claude plugin skills)"
128
+ echo " ~/.codex/skills/ (Codex skills)"
129
+ echo " ./.agents/skills/ (project-level skills, current directory)"
130
+ echo ""
131
+ echo "Output:"
132
+ echo " ~/.cursor/commands/skill-tags.md (full index of all skills)"
133
+ echo " ~/.cursor/commands/skills-<category>.md (generated by --categories)"
134
+ echo ""
135
+ echo "Category config:"
136
+ echo " ~/.cursor/skill-tags-categories.conf"
137
+ exit 0
138
+ ;;
139
+ esac
140
+ done
141
+
142
+ # ─── Helpers ───────────────────────────────────────────────────────────────────
143
+
144
+ count_found=0
145
+ count_dupes=0
146
+
147
+ log() { printf " %s\n" "$*"; }
148
+ success() { printf " ✓ %s\n" "$*"; }
149
+
150
+ # Tracks which skill names have already been processed (deduplication).
151
+ # Uses a delimited string for bash 3.2 compatibility (no associative arrays).
152
+ seen_skills=":"
153
+
154
+ # Convert kebab-case or snake_case to Title Case
155
+ to_title_case() {
156
+ echo "$1" | sed 's/[-_]/ /g' | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) substr($i,2)}1'
157
+ }
158
+
159
+ # Extract description: from YAML frontmatter, or fall back to first content line
160
+ extract_description() {
161
+ local skill_file="$1"
162
+ local desc
163
+
164
+ desc=$(awk '
165
+ BEGIN { in_fm=0 }
166
+ /^---/ {
167
+ if (in_fm == 0) { in_fm=1; next }
168
+ else { exit }
169
+ }
170
+ in_fm==1 && /^description:/ {
171
+ sub(/^description:[[:space:]]*/, "")
172
+ gsub(/^["'"'"']|["'"'"']$/, "")
173
+ print
174
+ exit
175
+ }
176
+ ' "$skill_file" 2>/dev/null)
177
+
178
+ if [[ -n "$desc" ]]; then
179
+ echo "$desc"
180
+ return
181
+ fi
182
+
183
+ desc=$(awk '
184
+ BEGIN { in_fm=0 }
185
+ /^---/ {
186
+ if (in_fm == 0) { in_fm=1; next }
187
+ else { in_fm=0; next }
188
+ }
189
+ in_fm { next }
190
+ /^#/ { next }
191
+ /^[[:space:]]*$/ { next }
192
+ { print; exit }
193
+ ' "$skill_file" 2>/dev/null)
194
+
195
+ echo "${desc:-(No description available)}"
196
+ }
197
+
198
+ # Extract metadata.tags from YAML frontmatter.
199
+ # Returns a colon-delimited string, e.g. "frontend:react:animation"
200
+ extract_metadata_tags() {
201
+ local skill_file="$1"
202
+ awk '
203
+ BEGIN { in_fm=0; in_meta=0 }
204
+ /^---/ { if (in_fm==0) { in_fm=1; next } else { exit } }
205
+ in_fm && /^metadata:/ { in_meta=1; next }
206
+ in_meta && /^[^ ]/ { in_meta=0 }
207
+ in_meta && /tags:/ {
208
+ gsub(/.*tags:[[:space:]]*/, "")
209
+ gsub(/[\[\]]/, "")
210
+ gsub(/,/, ":")
211
+ gsub(/[[:space:]]/, "")
212
+ print
213
+ exit
214
+ }
215
+ ' "$skill_file" 2>/dev/null
216
+ }
217
+
218
+ collect_skill() {
219
+ local skill_dir="$1"
220
+ local skill_name="$2"
221
+ local skill_file="${skill_dir}/SKILL.md"
222
+ local display_path="${skill_dir/#$HOME/~}"
223
+ local title
224
+ title="$(to_title_case "$skill_name")"
225
+ local desc
226
+ desc="$(extract_description "$skill_file")"
227
+ local tags
228
+ tags="$(extract_metadata_tags "$skill_file")"
229
+
230
+ # Markdown section for skill-tags.md
231
+ cat >> "$SKILLS_TEMP" <<EOF
232
+
233
+ ### ${title}
234
+ \`${display_path}\`
235
+
236
+ ${desc}
237
+ EOF
238
+
239
+ # Pipe-delimited metadata record for categorization lookups
240
+ # Format: display_path|description|tags
241
+ printf '%s|%s|%s\n' "$display_path" "$desc" "$tags" > "${SKILLS_META_DIR}/${skill_name}"
242
+
243
+ count_found=$(( count_found + 1 ))
244
+ log " Found: ${title} (${display_path})"
245
+ }
246
+
247
+ # Recursively find all SKILL.md files under a directory tree.
248
+ # Handles flat dirs (~/.agents/skills/name/SKILL.md) and
249
+ # nested plugin caches (cache/plugin/version/skills/name/SKILL.md).
250
+ scan_tree() {
251
+ local base_dir="$1"
252
+
253
+ [[ -d "$base_dir" ]] || return 0
254
+
255
+ local found=0
256
+ local base_dir_slash="${base_dir}/"
257
+
258
+ while IFS= read -r skill_file; do
259
+ # Check only the relative portion of the path for hidden components.
260
+ # The base_dir itself may live inside hidden dirs (e.g. ~/.agents/) so we
261
+ # must not apply the dot-filter to the full absolute path.
262
+ local rel="${skill_file#$base_dir_slash}"
263
+ if echo "$rel" | grep -q '/\.'; then
264
+ continue
265
+ fi
266
+
267
+ local skill_dir
268
+ skill_dir="$(dirname "$skill_file")"
269
+ local skill_name
270
+ skill_name="$(basename "$skill_dir")"
271
+
272
+ # Skip if this skill name was already processed from a higher-priority source
273
+ if [[ "$seen_skills" == *":${skill_name}:"* ]]; then
274
+ count_dupes=$(( count_dupes + 1 ))
275
+ continue
276
+ fi
277
+
278
+ seen_skills="${seen_skills}${skill_name}:"
279
+ collect_skill "$skill_dir" "$skill_name"
280
+ found=$(( found + 1 ))
281
+ done < <(find "$base_dir" -name "SKILL.md" 2>/dev/null | sort)
282
+
283
+ if [[ $found -gt 0 ]]; then
284
+ log " Found $found skill(s) in ${base_dir/#$HOME/~}"
285
+ fi
286
+ }
287
+
288
+ # ─── Category helpers ───────────────────────────────────────────────────────────
289
+
290
+ PREDEFINED_CATEGORIES="frontend backend database testing accessibility performance ai-agents devops design"
291
+
292
+ # Hardcoded keyword map for Tier 2 fallback categorization.
293
+ # Returns a colon-delimited keyword string for the given category name.
294
+ get_category_keywords() {
295
+ case "$1" in
296
+ frontend) echo "react:next:vue:svelte:css:html:responsive:component:ui:ux:tailwind:animation:design-system" ;;
297
+ backend) echo "api:server:node:express:rest:graphql:stripe:payment:webhook" ;;
298
+ database) echo "postgres:sql:supabase:database:schema:query:migration:orm" ;;
299
+ testing) echo "test:vitest:playwright:jest:coverage:mock:spec:e2e" ;;
300
+ accessibility) echo "accessibility:aria:wcag:a11y:screen-reader:keyboard" ;;
301
+ performance) echo "performance:optimize:speed:lazy:cache:memoize:bundle:lighthouse" ;;
302
+ ai-agents) echo "agent:skill:claude:cursor:mcp:subagent:browser:automation:llm:ai:workflow" ;;
303
+ devops) echo "deploy:vercel:docker:ci:cd:build:pipeline:github:actions" ;;
304
+ design) echo "design:figma:ux:animation:motion:color:typography:shadow:gradient" ;;
305
+ *) echo "" ;;
306
+ esac
307
+ }
308
+
309
+ # Two-tier categorization check for a single skill against a category.
310
+ # Tier 1: metadata.tags match (higher confidence)
311
+ # Tier 2: keyword match against skill name + description (fallback)
312
+ # Prints "1" for tier-1 match, "2" for tier-2 match, "" for no match.
313
+ # All interactive output must go to stderr; stdout is reserved for the return value.
314
+ skill_match_tier() {
315
+ local skill_name="$1"
316
+ local category="$2"
317
+ local meta_file="${SKILLS_META_DIR}/${skill_name}"
318
+ [[ -f "$meta_file" ]] || return 0
319
+
320
+ local tags
321
+ tags="$(awk -F'|' '{print $3}' "$meta_file")"
322
+ local desc
323
+ desc="$(awk -F'|' '{print $2}' "$meta_file")"
324
+ local cat_keywords
325
+ cat_keywords="$(get_category_keywords "$category")"
326
+
327
+ # Tier 1 — metadata.tags
328
+ if [[ -n "$tags" && -n "$cat_keywords" ]]; then
329
+ local tag
330
+ while IFS= read -r tag; do
331
+ [[ -z "$tag" ]] && continue
332
+ if echo ":${cat_keywords}:" | grep -qi ":${tag}:"; then
333
+ echo "1"
334
+ return
335
+ fi
336
+ done < <(echo "$tags" | tr ':' '\n')
337
+ fi
338
+
339
+ # Tier 2 — keyword match against name + description
340
+ if [[ -n "$cat_keywords" ]]; then
341
+ local search_text="${skill_name} ${desc}"
342
+ local kw
343
+ while IFS= read -r kw; do
344
+ [[ -z "$kw" ]] && continue
345
+ if echo "$search_text" | grep -qi "$kw"; then
346
+ echo "2"
347
+ return
348
+ fi
349
+ done < <(echo "$cat_keywords" | tr ':' '\n')
350
+ fi
351
+ }
352
+
353
+ # Read current skill assignments for a category from the config file.
354
+ read_config_category() {
355
+ local category="$1"
356
+ if [[ ! -f "$CATEGORIES_CONFIG" ]]; then
357
+ echo ""
358
+ return
359
+ fi
360
+ grep "^${category}=" "$CATEGORIES_CONFIG" 2>/dev/null | sed "s/^${category}=//" || true
361
+ }
362
+
363
+ # Write (create or update) a category line in the config file.
364
+ write_config_category() {
365
+ local category="$1"
366
+ local skill_list="$2"
367
+ local tmp
368
+ tmp="$(mktemp)"
369
+
370
+ if [[ -f "$CATEGORIES_CONFIG" ]] && grep -q "^${category}=" "$CATEGORIES_CONFIG" 2>/dev/null; then
371
+ # Replace existing line
372
+ awk -v cat="$category" -v list="$skill_list" \
373
+ 'substr($0,1,length(cat)+1)==cat"=" { print cat"="list; next } { print }' \
374
+ "$CATEGORIES_CONFIG" > "$tmp"
375
+ else
376
+ # Append new line
377
+ if [[ -f "$CATEGORIES_CONFIG" ]]; then
378
+ cp "$CATEGORIES_CONFIG" "$tmp"
379
+ else
380
+ printf '# skill-tags category config — edit with: skill-tags --categories\n' > "$tmp"
381
+ fi
382
+ echo "${category}=${skill_list}" >> "$tmp"
383
+ fi
384
+
385
+ mv "$tmp" "$CATEGORIES_CONFIG"
386
+ }
387
+
388
+ # Remove a category from the config and delete its generated command file.
389
+ delete_config_category() {
390
+ local category="$1"
391
+ [[ -f "$CATEGORIES_CONFIG" ]] || return 0
392
+ local tmp
393
+ tmp="$(mktemp)"
394
+ grep -v "^${category}=" "$CATEGORIES_CONFIG" > "$tmp" || true
395
+ mv "$tmp" "$CATEGORIES_CONFIG"
396
+ rm -f "${GLOBAL_COMMANDS_DIR}/skills-${category}.md"
397
+ }
398
+
399
+ # Interactive skill-selection UI for a category.
400
+ # All display output goes to stderr; echoes the final comma-delimited skill list to stdout.
401
+ select_skills_for_category() {
402
+ local category="$1"
403
+ local current_assignments="$2"
404
+
405
+ local skill_names=()
406
+ local skill_selected=()
407
+ local skill_tiers=()
408
+
409
+ # Build the full list of skills from SKILLS_META_DIR
410
+ for meta_file in "${SKILLS_META_DIR}"/*; do
411
+ [[ -f "$meta_file" ]] || continue
412
+ local sname
413
+ sname="$(basename "$meta_file")"
414
+ skill_names+=("$sname")
415
+
416
+ local tier
417
+ tier="$(skill_match_tier "$sname" "$category")"
418
+ skill_tiers+=("$tier")
419
+
420
+ # Pre-select if already in current_assignments, or if there's a tier match and
421
+ # this is a fresh category (no prior assignments).
422
+ if echo ":${current_assignments}:" | grep -q ":${sname}:"; then
423
+ skill_selected+=("1")
424
+ elif [[ -n "$tier" && -z "$current_assignments" ]]; then
425
+ skill_selected+=("1")
426
+ else
427
+ skill_selected+=("")
428
+ fi
429
+ done
430
+
431
+ while true; do
432
+ printf "\n Category: %s\n" "$category" >&2
433
+ printf " Skills (toggle by number, Enter to confirm):\n\n" >&2
434
+
435
+ local i=0
436
+ while [[ $i -lt ${#skill_names[@]} ]]; do
437
+ local sname="${skill_names[$i]}"
438
+ local tier="${skill_tiers[$i]}"
439
+ local sel="${skill_selected[$i]}"
440
+ local num=$(( i + 1 ))
441
+
442
+ local marker="[ ]"
443
+ [[ "$sel" == "1" ]] && marker="[*]"
444
+
445
+ local hint=""
446
+ local tags
447
+ tags="$(awk -F'|' '{print $3}' "${SKILLS_META_DIR}/${sname}" 2>/dev/null || true)"
448
+ if [[ "$tier" == "1" && -n "$tags" ]]; then
449
+ local readable_tags
450
+ readable_tags="$(echo "$tags" | tr ':' ', ' | sed 's/, $//')"
451
+ hint=" (metadata.tags: ${readable_tags})"
452
+ elif [[ "$tier" == "2" ]]; then
453
+ hint=" (keyword match)"
454
+ fi
455
+
456
+ printf " %s %3d %-45s%s\n" "$marker" "$num" "$sname" "$hint" >&2
457
+ i=$(( i + 1 ))
458
+ done
459
+
460
+ printf "\n Toggle by number (space-separated) or Enter to confirm: " >&2
461
+ local input
462
+ read -r input
463
+
464
+ if [[ -z "$input" ]]; then
465
+ break
466
+ fi
467
+
468
+ for num in $input; do
469
+ if echo "$num" | grep -q '^[0-9][0-9]*$'; then
470
+ local idx=$(( num - 1 ))
471
+ if [[ $idx -ge 0 && $idx -lt ${#skill_names[@]} ]]; then
472
+ if [[ "${skill_selected[$idx]}" == "1" ]]; then
473
+ skill_selected[$idx]=""
474
+ else
475
+ skill_selected[$idx]="1"
476
+ fi
477
+ fi
478
+ fi
479
+ done
480
+ done
481
+
482
+ # Return the final comma-delimited list via stdout
483
+ local result=""
484
+ local i=0
485
+ while [[ $i -lt ${#skill_names[@]} ]]; do
486
+ if [[ "${skill_selected[$i]}" == "1" ]]; then
487
+ [[ -n "$result" ]] && result="${result},"
488
+ result="${result}${skill_names[$i]}"
489
+ fi
490
+ i=$(( i + 1 ))
491
+ done
492
+
493
+ echo "$result"
494
+ }
495
+
496
+ # ─── Category command ───────────────────────────────────────────────────────────
497
+
498
+ cmd_categories() {
499
+ printf "\n skill-tags: category wizard\n\n"
500
+ printf " Scanning all skills...\n"
501
+
502
+ for entry in "${GLOBAL_SKILL_SOURCES[@]}"; do
503
+ local dir="${entry%%:*}"
504
+ [[ -d "$dir" ]] && scan_tree "$dir"
505
+ done
506
+ if [[ "$GLOBAL_ONLY" == "false" && -d ".agents/skills" ]]; then
507
+ scan_tree "$(pwd)/.agents/skills"
508
+ fi
509
+
510
+ printf " Found %d skill(s)\n" "$count_found"
511
+
512
+ mkdir -p "$GLOBAL_COMMANDS_DIR"
513
+ if [[ ! -f "$CATEGORIES_CONFIG" ]]; then
514
+ printf '# skill-tags category config — edit with: skill-tags --categories\n' > "$CATEGORIES_CONFIG"
515
+ fi
516
+
517
+ # Main CRUD loop
518
+ while true; do
519
+ printf "\n Current categories:\n"
520
+
521
+ local cat_names=()
522
+ local has_cats=false
523
+
524
+ if [[ -f "$CATEGORIES_CONFIG" ]]; then
525
+ while IFS='=' read -r cat_name skill_list; do
526
+ [[ "$cat_name" == "#"* || -z "$cat_name" ]] && continue
527
+ cat_names+=("$cat_name")
528
+ local count
529
+ count="$(echo "$skill_list" | awk -F',' '{print NF}')"
530
+ [[ -z "$skill_list" ]] && count=0
531
+ printf " %d) %s (%s skills)\n" "${#cat_names[@]}" "$cat_name" "$count"
532
+ has_cats=true
533
+ done < "$CATEGORIES_CONFIG"
534
+ fi
535
+
536
+ if [[ "$has_cats" == "false" ]]; then
537
+ printf " (none yet)\n"
538
+ fi
539
+
540
+ printf "\n [a] Add [e] Edit [d] Delete [s] Save & generate [q] Quit\n"
541
+ printf " > "
542
+ local action
543
+ read -r action
544
+
545
+ case "$action" in
546
+ a|A)
547
+ # Show predefined category list
548
+ printf "\n Predefined categories:\n"
549
+ local pcat_names=()
550
+ local pcat
551
+ for pcat in $PREDEFINED_CATEGORIES; do
552
+ pcat_names+=("$pcat")
553
+ printf " %d) %s\n" "${#pcat_names[@]}" "$pcat"
554
+ done
555
+ printf " c) custom\n"
556
+ printf " > "
557
+ local choice
558
+ read -r choice
559
+
560
+ local new_cat=""
561
+ if [[ "$choice" == "c" || "$choice" == "C" ]]; then
562
+ printf " Category name (lowercase, hyphens ok): "
563
+ read -r new_cat
564
+ new_cat="$(echo "$new_cat" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g' | sed 's/--*/-/g' | sed 's/^-//' | sed 's/-$//')"
565
+ elif echo "$choice" | grep -q '^[0-9][0-9]*$'; then
566
+ local pidx=$(( choice - 1 ))
567
+ if [[ $pidx -ge 0 && $pidx -lt ${#pcat_names[@]} ]]; then
568
+ new_cat="${pcat_names[$pidx]}"
569
+ fi
570
+ fi
571
+
572
+ if [[ -n "$new_cat" ]]; then
573
+ local current
574
+ current="$(read_config_category "$new_cat")"
575
+ local result
576
+ result="$(select_skills_for_category "$new_cat" "$current")"
577
+ write_config_category "$new_cat" "$result"
578
+ printf "\n ✓ Saved: %s\n" "$new_cat"
579
+ fi
580
+ ;;
581
+
582
+ e|E)
583
+ if [[ ${#cat_names[@]} -eq 0 ]]; then
584
+ printf " No categories yet. Use [a] to add one.\n"
585
+ continue
586
+ fi
587
+ printf " Edit which category? (number): "
588
+ local edit_num
589
+ read -r edit_num
590
+ if echo "$edit_num" | grep -q '^[0-9][0-9]*$'; then
591
+ local eidx=$(( edit_num - 1 ))
592
+ if [[ $eidx -ge 0 && $eidx -lt ${#cat_names[@]} ]]; then
593
+ local edit_cat="${cat_names[$eidx]}"
594
+ local current
595
+ current="$(read_config_category "$edit_cat")"
596
+ local result
597
+ result="$(select_skills_for_category "$edit_cat" "$current")"
598
+ write_config_category "$edit_cat" "$result"
599
+ printf "\n ✓ Updated: %s\n" "$edit_cat"
600
+ fi
601
+ fi
602
+ ;;
603
+
604
+ d|D)
605
+ if [[ ${#cat_names[@]} -eq 0 ]]; then
606
+ printf " No categories yet.\n"
607
+ continue
608
+ fi
609
+ printf " Delete which category? (number): "
610
+ local del_num
611
+ read -r del_num
612
+ if echo "$del_num" | grep -q '^[0-9][0-9]*$'; then
613
+ local didx=$(( del_num - 1 ))
614
+ if [[ $didx -ge 0 && $didx -lt ${#cat_names[@]} ]]; then
615
+ local del_cat="${cat_names[$didx]}"
616
+ printf " Delete '%s' and its generated file? [y/N] " "$del_cat"
617
+ local confirm
618
+ read -r confirm
619
+ local confirm_lower
620
+ confirm_lower="$(echo "$confirm" | tr '[:upper:]' '[:lower:]')"
621
+ if [[ "$confirm_lower" == "y" ]]; then
622
+ delete_config_category "$del_cat"
623
+ printf " ✓ Deleted: %s\n" "$del_cat"
624
+ fi
625
+ fi
626
+ fi
627
+ ;;
628
+
629
+ s|S)
630
+ printf "\n Generating category files...\n"
631
+ generate_category_files
632
+ printf "\n Done. Run 'skill-tags' to rebuild the full index.\n\n"
633
+ exit 0
634
+ ;;
635
+
636
+ q|Q|"")
637
+ printf "\n Exiting without generating files.\n\n"
638
+ exit 0
639
+ ;;
640
+ esac
641
+ done
642
+ }
643
+
644
+ # ─── Generate category files ────────────────────────────────────────────────────
645
+
646
+ # Reads ~/.cursor/skill-tags-categories.conf and writes a skills-<category>.md
647
+ # command file for each category. Called automatically on every sync when config exists.
648
+ generate_category_files() {
649
+ [[ -f "$CATEGORIES_CONFIG" ]] || return 0
650
+
651
+ local gen_count=0
652
+
653
+ while IFS='=' read -r cat_name skill_list; do
654
+ [[ "$cat_name" == "#"* || -z "$cat_name" ]] && continue
655
+ [[ -z "$skill_list" ]] && continue
656
+
657
+ local title
658
+ title="$(to_title_case "$cat_name")"
659
+ local out="${GLOBAL_COMMANDS_DIR}/skills-${cat_name}.md"
660
+
661
+ local skills_section=""
662
+ while IFS= read -r sname; do
663
+ [[ -z "$sname" ]] && continue
664
+ local meta_file="${SKILLS_META_DIR}/${sname}"
665
+ if [[ -f "$meta_file" ]]; then
666
+ local display_path desc stitle
667
+ display_path="$(awk -F'|' '{print $1}' "$meta_file")"
668
+ desc="$(awk -F'|' '{print $2}' "$meta_file")"
669
+ stitle="$(to_title_case "$sname")"
670
+ skills_section="${skills_section}
671
+ ### ${stitle}
672
+ \`${display_path}\`
673
+
674
+ ${desc}
675
+ "
676
+ fi
677
+ done < <(echo "$skill_list" | tr ',' '\n')
678
+
679
+ cat > "$out" <<EOF
680
+ # Skills: ${title}
681
+
682
+ <!-- Auto-generated by sync.sh (skill-tags) v${VERSION} — do not edit manually -->
683
+
684
+ Assess the following ${title} skills and apply any that are relevant to completing the user's request.
685
+
686
+ CRITICAL REQUIREMENT: Before applying any skill, you MUST use the Read tool to read the full contents of the skill file at the provided path. Do not assume the skill's behavior from its title or description alone.
687
+
688
+ If operating in Plan Mode, explicitly reference specific skills and subagents within the plan contents and TODOs.
689
+
690
+ ## ${title} Skills
691
+ ${skills_section}
692
+ EOF
693
+
694
+ gen_count=$(( gen_count + 1 ))
695
+ success "Generated: ${out/#$HOME/~}"
696
+ done < "$CATEGORIES_CONFIG"
697
+
698
+ if [[ $gen_count -gt 0 ]]; then
699
+ printf " Category files: %d generated\n" "$gen_count"
700
+ fi
701
+ }
702
+
703
+ # ─── Main ──────────────────────────────────────────────────────────────────────
704
+
705
+ if [[ "$RUN_CATEGORIES" == "true" ]]; then
706
+ cmd_categories
707
+ exit 0
708
+ fi
709
+
710
+ printf "\n🔄 Cursor Skill Command Sync v%s\n\n" "$VERSION"
711
+
712
+ # 1. Scan all global/user-level skill sources
713
+ for entry in "${GLOBAL_SKILL_SOURCES[@]}"; do
714
+ dir="${entry%%:*}"
715
+ label="${entry##*:}"
716
+ if [[ -d "$dir" ]]; then
717
+ log "Scanning ${label}: ${dir/#$HOME/~}"
718
+ scan_tree "$dir"
719
+ fi
720
+ done
721
+
722
+ # 2. Project-level skills (.agents/skills in CWD)
723
+ if [[ "$GLOBAL_ONLY" == "false" && -d ".agents/skills" ]]; then
724
+ log "Scanning project skills: $(pwd)/.agents/skills"
725
+ scan_tree "$(pwd)/.agents/skills"
726
+ fi
727
+
728
+ # ─── Write skill-tags.md ───────────────────────────────────────────────────────
729
+
730
+ mkdir -p "$GLOBAL_COMMANDS_DIR"
731
+
732
+ OPENING="Assess the following skills available in this workspace and apply any that are relevant to completing the user's request at the highest level of efficiency, quality, and completeness. When skills overlap in scope, assess the overlapping skills in greater detail and autonomously determine which is the best match for the project or the specific request — do not prompt the user to resolve overlaps.
733
+
734
+ CRITICAL REQUIREMENT: Before applying any skill, you MUST use the Read tool to read the full contents of the skill file at the provided path. Do not assume the skill's behavior from its title or description alone.
735
+
736
+ If operating in Plan Mode, explicitly include references to specific skills to use and (if applicable) subagents to utilize for efficient programming within the contents of the plan and the plan's TODOs.
737
+
738
+ Examples:
739
+ - \"Use the \`responsive-design/SKILL.md\` to apply advanced clamp-based responsiveness to the new navigation bar.\"
740
+ - \"Delegate to the \`frontend-designer\` subagent using \`ui-ux-pro-max/SKILL.md\` to build the polished component.\"
741
+ - \"Utilize \`supabase-postgres-best-practices/SKILL.md\` when designing the database schema for the user profiles.\""
742
+
743
+ is_update=false
744
+ [[ -f "$OUTPUT_FILE" ]] && is_update=true
745
+
746
+ cat > "$OUTPUT_FILE" <<EOF
747
+ # Skill Tags Command
748
+
749
+ <!-- Auto-generated by sync.sh (skill-tags) v${VERSION} — do not edit manually -->
750
+
751
+ ${OPENING}
752
+
753
+ ## Available Skills
754
+ $(cat "$SKILLS_TEMP")
755
+ EOF
756
+
757
+ # ─── Generate category files (if config exists) ────────────────────────────────
758
+
759
+ generate_category_files
760
+
761
+ # ─── Summary ───────────────────────────────────────────────────────────────────
762
+
763
+ printf "\n"
764
+ if [[ "$is_update" == "true" ]]; then
765
+ printf " ↺ Updated: %s\n" "${OUTPUT_FILE/#$HOME/~}"
766
+ else
767
+ printf " ✓ Generated: %s\n" "${OUTPUT_FILE/#$HOME/~}"
768
+ fi
769
+ printf " Skills: %d indexed\n" "$count_found"
770
+ if [[ $count_dupes -gt 0 ]]; then
771
+ printf " Dupes: %d skill(s) skipped (covered by higher-priority source)\n" "$count_dupes"
772
+ fi
773
+ printf "\n Tip: type /skill-tags in Cursor chat to load the full skills reference.\n\n"
package/uninstall.sh ADDED
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env bash
2
+ # uninstall.sh
3
+ # Removes skill-tags: deletes the sync script, removes the shell wrapper,
4
+ # and optionally deletes generated command files.
5
+
6
+ set -euo pipefail
7
+
8
+ SYNC_SCRIPT="${HOME}/.cursor/sync-skill-commands.sh"
9
+ COMMANDS_DIR="${HOME}/.cursor/commands"
10
+ MARKER="# ─── skill-tags / Cursor Skill Command Sync"
11
+
12
+ # Detect shell rc file
13
+ detect_rc() {
14
+ if [[ -n "${ZSH_VERSION:-}" ]] || [[ "$(basename "${SHELL:-}")" == "zsh" ]]; then
15
+ echo "${HOME}/.zshrc"
16
+ else
17
+ echo "${HOME}/.bash_profile"
18
+ fi
19
+ }
20
+
21
+ RC_FILE="$(detect_rc)"
22
+
23
+ printf "\n🗑 skill-tags — Uninstaller\n\n"
24
+
25
+ # 1. Remove sync script
26
+ if [[ -f "$SYNC_SCRIPT" ]]; then
27
+ rm "$SYNC_SCRIPT"
28
+ printf " ✓ Removed %s\n" "${SYNC_SCRIPT/#$HOME/~}"
29
+ else
30
+ printf " ~ Sync script not found (already removed?)\n"
31
+ fi
32
+
33
+ # 2. Remove shell wrapper from rc file
34
+ if grep -q "$MARKER" "$RC_FILE" 2>/dev/null; then
35
+ # Remove the block from the marker line through the closing marker line
36
+ local_tmp="$(mktemp)"
37
+ awk "
38
+ /${MARKER//\//\\/}/{found=1}
39
+ found && /^# ─────/{if(++count==2){found=0; next}}
40
+ !found
41
+ " "$RC_FILE" > "$local_tmp"
42
+ mv "$local_tmp" "$RC_FILE"
43
+ printf " ✓ Removed skills wrapper from %s\n" "${RC_FILE/#$HOME/~}"
44
+ else
45
+ printf " ~ Shell wrapper not found in %s\n" "${RC_FILE/#$HOME/~}"
46
+ fi
47
+
48
+ # 3. Optionally remove generated command files
49
+ printf "\n Remove generated command files in %s? [y/N] " "${COMMANDS_DIR/#$HOME/~}"
50
+ read -r answer
51
+ if [[ "${answer,,}" == "y" ]]; then
52
+ # Only remove auto-generated files (those with our comment marker)
53
+ removed=0
54
+ for f in "$COMMANDS_DIR"/*.md; do
55
+ [[ -f "$f" ]] || continue
56
+ if grep -q "Auto-generated by sync" "$f" 2>/dev/null; then
57
+ rm "$f"
58
+ removed=$(( removed + 1 ))
59
+ fi
60
+ done
61
+ printf " ✓ Removed %d generated command file(s)\n" "$removed"
62
+ else
63
+ printf " ~ Keeping generated command files.\n"
64
+ fi
65
+
66
+ printf "\n Done. Restart your shell or run: source %s\n\n" "${RC_FILE/#$HOME/~}"