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 +21 -0
- package/README.md +256 -0
- package/bin/postinstall.js +42 -0
- package/bin/skill-tags.js +44 -0
- package/install.sh +130 -0
- package/package.json +42 -0
- package/sync.sh +773 -0
- package/uninstall.sh +66 -0
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/~}"
|