@smitdev/ai-skills 0.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 +68 -0
- package/bin/cli.js +198 -0
- package/lib/adapters.js +102 -0
- package/lib/install.js +56 -0
- package/lib/skills.js +75 -0
- package/package.json +46 -0
- package/skills/contract/SKILL.md +150 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 smitdev
|
|
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,68 @@
|
|
|
1
|
+
# @smitdev/ai-skills
|
|
2
|
+
|
|
3
|
+
Reusable AI assistant **skills** you can install with one command — for
|
|
4
|
+
**Claude Code**, **GitHub Copilot**, **Cursor**, and **Windsurf**.
|
|
5
|
+
|
|
6
|
+
Skills are authored once in the Claude [Agent Skill](https://docs.claude.com/en/docs/claude-code/skills)
|
|
7
|
+
format (`SKILL.md`) and converted on install to whatever your assistant expects.
|
|
8
|
+
|
|
9
|
+
## Install a skill
|
|
10
|
+
|
|
11
|
+
No global install needed — run it with `npx`:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# See what's available
|
|
15
|
+
npx @smitdev/ai-skills list
|
|
16
|
+
|
|
17
|
+
# Interactive: pick assistant(s) and skill(s)
|
|
18
|
+
npx @smitdev/ai-skills install
|
|
19
|
+
|
|
20
|
+
# Non-interactive examples
|
|
21
|
+
npx @smitdev/ai-skills install --assistant claude --global
|
|
22
|
+
npx @smitdev/ai-skills install --assistant copilot,cursor,windsurf
|
|
23
|
+
npx @smitdev/ai-skills install --assistant all --skill contract --project
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Where files go
|
|
27
|
+
|
|
28
|
+
| Assistant | Scope | Destination |
|
|
29
|
+
|-----------------|------------------|-------------|
|
|
30
|
+
| Claude Code | `--global` | `~/.claude/skills/<name>/SKILL.md` |
|
|
31
|
+
| Claude Code | `--project` | `./.claude/skills/<name>/SKILL.md` |
|
|
32
|
+
| GitHub Copilot | project | `./.github/instructions/<name>.instructions.md` |
|
|
33
|
+
| Cursor | project | `./.cursor/rules/<name>.mdc` |
|
|
34
|
+
| Windsurf | project | `./.windsurf/rules/<name>.md` |
|
|
35
|
+
|
|
36
|
+
Copilot, Cursor, and Windsurf read their rules from inside a repo, so those
|
|
37
|
+
always install into a project directory (`--dir`, default: current folder).
|
|
38
|
+
Claude Code can install globally (available everywhere) or per-project.
|
|
39
|
+
|
|
40
|
+
Use `--dry-run` to preview without writing anything.
|
|
41
|
+
|
|
42
|
+
## Available skills
|
|
43
|
+
|
|
44
|
+
- **contract** — Create a build-ready spec / PRD for a feature before any code
|
|
45
|
+
is written, so a coding assistant can build it without guessing.
|
|
46
|
+
|
|
47
|
+
## Authoring your own skills
|
|
48
|
+
|
|
49
|
+
Each skill is a folder under [`skills/`](skills/) containing a `SKILL.md` with
|
|
50
|
+
YAML frontmatter:
|
|
51
|
+
|
|
52
|
+
```markdown
|
|
53
|
+
---
|
|
54
|
+
name: my-skill
|
|
55
|
+
description: When to use this skill, in one or two sentences. The assistant uses this to decide when to apply it.
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
# My Skill
|
|
59
|
+
|
|
60
|
+
Instructions for the assistant…
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Drop the folder in `skills/`, bump the version, and publish. Any extra files in
|
|
64
|
+
the folder are copied verbatim for Claude Code.
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
|
|
68
|
+
MIT
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const readline = require('readline');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { listSkills } = require('../lib/skills');
|
|
7
|
+
const { adapters } = require('../lib/adapters');
|
|
8
|
+
const { install } = require('../lib/install');
|
|
9
|
+
|
|
10
|
+
const ASSISTANT_IDS = Object.keys(adapters);
|
|
11
|
+
|
|
12
|
+
function parseArgs(argv) {
|
|
13
|
+
const args = { _: [], flags: {} };
|
|
14
|
+
for (let i = 0; i < argv.length; i++) {
|
|
15
|
+
const a = argv[i];
|
|
16
|
+
if (a.startsWith('--')) {
|
|
17
|
+
const key = a.slice(2);
|
|
18
|
+
const next = argv[i + 1];
|
|
19
|
+
if (next === undefined || next.startsWith('--')) {
|
|
20
|
+
args.flags[key] = true;
|
|
21
|
+
} else {
|
|
22
|
+
args.flags[key] = next;
|
|
23
|
+
i++;
|
|
24
|
+
}
|
|
25
|
+
} else if (a === '-y') {
|
|
26
|
+
args.flags.yes = true;
|
|
27
|
+
} else {
|
|
28
|
+
args._.push(a);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return args;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function ask(rl, question) {
|
|
35
|
+
return new Promise((resolve) => rl.question(question, (a) => resolve(a.trim())));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parseList(val, valid) {
|
|
39
|
+
if (val === true || !val) return [];
|
|
40
|
+
const items = String(val)
|
|
41
|
+
.split(',')
|
|
42
|
+
.map((s) => s.trim())
|
|
43
|
+
.filter(Boolean);
|
|
44
|
+
if (val === 'all' || items.includes('all')) return valid.slice();
|
|
45
|
+
return items;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function printHelp() {
|
|
49
|
+
console.log(`
|
|
50
|
+
ai-skills — install reusable AI assistant skills
|
|
51
|
+
|
|
52
|
+
Usage:
|
|
53
|
+
npx @smitdev/ai-skills <command> [options]
|
|
54
|
+
|
|
55
|
+
Commands:
|
|
56
|
+
list List the skills bundled in this package
|
|
57
|
+
install Install skills for one or more assistants
|
|
58
|
+
|
|
59
|
+
Options for "install":
|
|
60
|
+
--assistant <ids> Comma-separated: ${ASSISTANT_IDS.join(', ')} (or "all")
|
|
61
|
+
--skill <names> Comma-separated skill names (or "all"; default: all)
|
|
62
|
+
--global Install to your home dir (Claude Code only)
|
|
63
|
+
--project Install into a project (default)
|
|
64
|
+
--dir <path> Project directory (default: current directory)
|
|
65
|
+
--dry-run Show what would change without writing
|
|
66
|
+
-y, --yes Skip interactive prompts (use flags / defaults)
|
|
67
|
+
-h, --help Show this help
|
|
68
|
+
|
|
69
|
+
Examples:
|
|
70
|
+
npx @smitdev/ai-skills list
|
|
71
|
+
npx @smitdev/ai-skills install --assistant claude --global
|
|
72
|
+
npx @smitdev/ai-skills install --assistant copilot,cursor --skill contract
|
|
73
|
+
`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function pickFromList(rl, label, options) {
|
|
77
|
+
console.log(`\n${label}`);
|
|
78
|
+
options.forEach((o, i) => console.log(` ${i + 1}. ${o.label}`));
|
|
79
|
+
console.log(` (comma-separated numbers, or "a" for all)`);
|
|
80
|
+
const ans = await ask(rl, '> ');
|
|
81
|
+
if (ans.toLowerCase() === 'a' || ans === '') return options.map((o) => o.id);
|
|
82
|
+
const picked = ans
|
|
83
|
+
.split(',')
|
|
84
|
+
.map((s) => parseInt(s.trim(), 10))
|
|
85
|
+
.filter((n) => n >= 1 && n <= options.length)
|
|
86
|
+
.map((n) => options[n - 1].id);
|
|
87
|
+
return [...new Set(picked)];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function runInstall(args) {
|
|
91
|
+
const skills = listSkills();
|
|
92
|
+
if (skills.length === 0) {
|
|
93
|
+
console.error('No skills found in this package.');
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const projectDir = path.resolve(
|
|
98
|
+
typeof args.flags.dir === 'string' ? args.flags.dir : process.cwd()
|
|
99
|
+
);
|
|
100
|
+
const dryRun = !!args.flags['dry-run'];
|
|
101
|
+
const scope = args.flags.global ? 'global' : 'project';
|
|
102
|
+
|
|
103
|
+
let assistants = parseList(args.flags.assistant, ASSISTANT_IDS);
|
|
104
|
+
let skillNames = parseList(args.flags.skill, skills.map((s) => s.name));
|
|
105
|
+
|
|
106
|
+
const nonInteractive =
|
|
107
|
+
args.flags.yes || (assistants.length > 0);
|
|
108
|
+
|
|
109
|
+
if (!nonInteractive && process.stdin.isTTY) {
|
|
110
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
111
|
+
if (assistants.length === 0) {
|
|
112
|
+
assistants = await pickFromList(
|
|
113
|
+
rl,
|
|
114
|
+
'Which assistant(s)?',
|
|
115
|
+
ASSISTANT_IDS.map((id) => ({ id, label: adapters[id].label }))
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
if (skillNames.length === 0) {
|
|
119
|
+
skillNames = await pickFromList(
|
|
120
|
+
rl,
|
|
121
|
+
'Which skill(s)?',
|
|
122
|
+
skills.map((s) => ({ id: s.name, label: `${s.name} — ${truncate(s.description, 60)}` }))
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
rl.close();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (assistants.length === 0) assistants = ASSISTANT_IDS.slice();
|
|
129
|
+
if (skillNames.length === 0) skillNames = skills.map((s) => s.name);
|
|
130
|
+
|
|
131
|
+
const invalidA = assistants.filter((a) => !ASSISTANT_IDS.includes(a));
|
|
132
|
+
if (invalidA.length) {
|
|
133
|
+
console.error(`Unknown assistant(s): ${invalidA.join(', ')}`);
|
|
134
|
+
console.error(`Valid: ${ASSISTANT_IDS.join(', ')}`);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
const selectedSkills = skills.filter((s) => skillNames.includes(s.name));
|
|
138
|
+
const invalidS = skillNames.filter((n) => !skills.some((s) => s.name === n));
|
|
139
|
+
if (invalidS.length) {
|
|
140
|
+
console.error(`Unknown skill(s): ${invalidS.join(', ')}`);
|
|
141
|
+
console.error(`Valid: ${skills.map((s) => s.name).join(', ')}`);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const results = install({
|
|
146
|
+
assistants,
|
|
147
|
+
skills: selectedSkills,
|
|
148
|
+
scope,
|
|
149
|
+
projectDir,
|
|
150
|
+
dryRun,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const verb = dryRun ? 'Would install' : 'Installed';
|
|
154
|
+
console.log(`\n${verb} ${selectedSkills.length} skill(s) for ${assistants.length} assistant(s):\n`);
|
|
155
|
+
for (const r of results) {
|
|
156
|
+
console.log(` [${r.assistant}] ${r.skill} -> ${r.path} (${r.action})`);
|
|
157
|
+
}
|
|
158
|
+
if (dryRun) console.log('\n(dry run — no files were written)');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function truncate(s, n) {
|
|
162
|
+
s = String(s || '');
|
|
163
|
+
return s.length > n ? s.slice(0, n - 1) + '…' : s;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function main() {
|
|
167
|
+
const args = parseArgs(process.argv.slice(2));
|
|
168
|
+
const cmd = args._[0];
|
|
169
|
+
|
|
170
|
+
if (args.flags.help || args.flags.h || cmd === 'help' || !cmd) {
|
|
171
|
+
printHelp();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (cmd === 'list') {
|
|
176
|
+
const skills = listSkills();
|
|
177
|
+
console.log(`\n${skills.length} skill(s) available:\n`);
|
|
178
|
+
for (const s of skills) {
|
|
179
|
+
console.log(` ${s.name}`);
|
|
180
|
+
console.log(` ${truncate(s.description, 100)}\n`);
|
|
181
|
+
}
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (cmd === 'install') {
|
|
186
|
+
await runInstall(args);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
console.error(`Unknown command: ${cmd}`);
|
|
191
|
+
printHelp();
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
main().catch((err) => {
|
|
196
|
+
console.error(err.message || err);
|
|
197
|
+
process.exit(1);
|
|
198
|
+
});
|
package/lib/adapters.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Each adapter turns a parsed skill into one or more output targets:
|
|
8
|
+
* { path, content } -> write a single file
|
|
9
|
+
* { path, copyDir } -> recursively copy a source folder (native format)
|
|
10
|
+
*
|
|
11
|
+
* `scope` is 'global' (user home), 'project' (a repo), or 'both'.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// YAML-safe scalar: JSON double-quoted strings are valid YAML double-quoted
|
|
15
|
+
// scalars, so this handles colons, quotes, and apostrophes in descriptions.
|
|
16
|
+
function yamlString(s) {
|
|
17
|
+
return JSON.stringify(String(s == null ? '' : s));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const claude = {
|
|
21
|
+
id: 'claude',
|
|
22
|
+
label: 'Claude Code',
|
|
23
|
+
scope: 'both',
|
|
24
|
+
// Native format — copy the whole skill folder verbatim.
|
|
25
|
+
resolve(skill, { scope, projectDir }) {
|
|
26
|
+
const base =
|
|
27
|
+
scope === 'global'
|
|
28
|
+
? path.join(os.homedir(), '.claude', 'skills')
|
|
29
|
+
: path.join(projectDir, '.claude', 'skills');
|
|
30
|
+
return [{ path: path.join(base, skill.name), copyDir: skill.dir }];
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const copilot = {
|
|
35
|
+
id: 'copilot',
|
|
36
|
+
label: 'GitHub Copilot',
|
|
37
|
+
scope: 'project',
|
|
38
|
+
resolve(skill, { projectDir }) {
|
|
39
|
+
const content =
|
|
40
|
+
`---\n` +
|
|
41
|
+
`description: ${yamlString(skill.description)}\n` +
|
|
42
|
+
`applyTo: "**"\n` +
|
|
43
|
+
`---\n\n` +
|
|
44
|
+
skill.body;
|
|
45
|
+
return [
|
|
46
|
+
{
|
|
47
|
+
path: path.join(
|
|
48
|
+
projectDir,
|
|
49
|
+
'.github',
|
|
50
|
+
'instructions',
|
|
51
|
+
`${skill.name}.instructions.md`
|
|
52
|
+
),
|
|
53
|
+
content,
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const cursor = {
|
|
60
|
+
id: 'cursor',
|
|
61
|
+
label: 'Cursor',
|
|
62
|
+
scope: 'project',
|
|
63
|
+
resolve(skill, { projectDir }) {
|
|
64
|
+
const content =
|
|
65
|
+
`---\n` +
|
|
66
|
+
`description: ${yamlString(skill.description)}\n` +
|
|
67
|
+
`globs:\n` +
|
|
68
|
+
`alwaysApply: false\n` +
|
|
69
|
+
`---\n\n` +
|
|
70
|
+
skill.body;
|
|
71
|
+
return [
|
|
72
|
+
{
|
|
73
|
+
path: path.join(projectDir, '.cursor', 'rules', `${skill.name}.mdc`),
|
|
74
|
+
content,
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const windsurf = {
|
|
81
|
+
id: 'windsurf',
|
|
82
|
+
label: 'Windsurf',
|
|
83
|
+
scope: 'project',
|
|
84
|
+
resolve(skill, { projectDir }) {
|
|
85
|
+
const content =
|
|
86
|
+
`---\n` +
|
|
87
|
+
`trigger: model_decision\n` +
|
|
88
|
+
`description: ${yamlString(skill.description)}\n` +
|
|
89
|
+
`---\n\n` +
|
|
90
|
+
skill.body;
|
|
91
|
+
return [
|
|
92
|
+
{
|
|
93
|
+
path: path.join(projectDir, '.windsurf', 'rules', `${skill.name}.md`),
|
|
94
|
+
content,
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const adapters = { claude, copilot, cursor, windsurf };
|
|
101
|
+
|
|
102
|
+
module.exports = { adapters, yamlString };
|
package/lib/install.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { adapters } = require('./adapters');
|
|
6
|
+
|
|
7
|
+
function copyDir(src, dest) {
|
|
8
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
9
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
10
|
+
const s = path.join(src, entry.name);
|
|
11
|
+
const d = path.join(dest, entry.name);
|
|
12
|
+
if (entry.isDirectory()) copyDir(s, d);
|
|
13
|
+
else fs.copyFileSync(s, d);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function writeFile(file, content) {
|
|
18
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
19
|
+
fs.writeFileSync(file, content);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Install the given skills for the given assistants.
|
|
24
|
+
* opts: { assistants: string[], skills: Skill[], scope, projectDir, dryRun }
|
|
25
|
+
* Returns a list of { assistant, skill, path, action } records.
|
|
26
|
+
*/
|
|
27
|
+
function install({ assistants, skills, scope, projectDir, dryRun }) {
|
|
28
|
+
const results = [];
|
|
29
|
+
for (const id of assistants) {
|
|
30
|
+
const adapter = adapters[id];
|
|
31
|
+
if (!adapter) throw new Error(`Unknown assistant: ${id}`);
|
|
32
|
+
|
|
33
|
+
// Project-only adapters ignore a 'global' scope request.
|
|
34
|
+
const effScope = adapter.scope === 'project' ? 'project' : scope;
|
|
35
|
+
|
|
36
|
+
for (const skill of skills) {
|
|
37
|
+
const targets = adapter.resolve(skill, { scope: effScope, projectDir });
|
|
38
|
+
for (const t of targets) {
|
|
39
|
+
const existed = fs.existsSync(t.path);
|
|
40
|
+
if (!dryRun) {
|
|
41
|
+
if (t.copyDir) copyDir(t.copyDir, t.path);
|
|
42
|
+
else writeFile(t.path, t.content);
|
|
43
|
+
}
|
|
44
|
+
results.push({
|
|
45
|
+
assistant: adapter.label,
|
|
46
|
+
skill: skill.name,
|
|
47
|
+
path: t.path,
|
|
48
|
+
action: existed ? 'updated' : 'created',
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return results;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { install, copyDir };
|
package/lib/skills.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const SKILLS_DIR = path.join(__dirname, '..', 'skills');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Minimal YAML frontmatter parser. Handles the flat `key: value` pairs we use
|
|
10
|
+
* in SKILL.md (name, description). Values run to end of line; surrounding
|
|
11
|
+
* quotes are stripped. This is intentionally tiny so the installer ships with
|
|
12
|
+
* zero dependencies.
|
|
13
|
+
*/
|
|
14
|
+
function parseFrontmatter(raw) {
|
|
15
|
+
const fm = {};
|
|
16
|
+
const lines = raw.split(/\r?\n/);
|
|
17
|
+
let key = null;
|
|
18
|
+
for (const line of lines) {
|
|
19
|
+
const m = line.match(/^([A-Za-z0-9_-]+):\s?(.*)$/);
|
|
20
|
+
if (m) {
|
|
21
|
+
key = m[1];
|
|
22
|
+
fm[key] = stripQuotes(m[2].trim());
|
|
23
|
+
} else if (key && /^\s+\S/.test(line)) {
|
|
24
|
+
// continuation line for a folded value
|
|
25
|
+
fm[key] = (fm[key] + ' ' + line.trim()).trim();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return fm;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function stripQuotes(v) {
|
|
32
|
+
if (
|
|
33
|
+
(v.startsWith('"') && v.endsWith('"')) ||
|
|
34
|
+
(v.startsWith("'") && v.endsWith("'"))
|
|
35
|
+
) {
|
|
36
|
+
return v.slice(1, -1);
|
|
37
|
+
}
|
|
38
|
+
return v;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parse a single skill folder into { name, description, body, dir }.
|
|
43
|
+
* Only the FIRST frontmatter block (the top of SKILL.md) is stripped — any
|
|
44
|
+
* `---` inside fenced code blocks in the body is preserved.
|
|
45
|
+
*/
|
|
46
|
+
function parseSkill(dir) {
|
|
47
|
+
const file = path.join(dir, 'SKILL.md');
|
|
48
|
+
const md = fs.readFileSync(file, 'utf8');
|
|
49
|
+
const m = md.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
50
|
+
let fm = {};
|
|
51
|
+
let body = md;
|
|
52
|
+
if (m) {
|
|
53
|
+
fm = parseFrontmatter(m[1]);
|
|
54
|
+
body = md.slice(m[0].length);
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
name: fm.name || path.basename(dir),
|
|
58
|
+
description: fm.description || '',
|
|
59
|
+
body: body.replace(/^\s+/, ''),
|
|
60
|
+
dir,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Discover every skill folder under skills/. */
|
|
65
|
+
function listSkills() {
|
|
66
|
+
if (!fs.existsSync(SKILLS_DIR)) return [];
|
|
67
|
+
return fs
|
|
68
|
+
.readdirSync(SKILLS_DIR, { withFileTypes: true })
|
|
69
|
+
.filter((d) => d.isDirectory())
|
|
70
|
+
.filter((d) => fs.existsSync(path.join(SKILLS_DIR, d.name, 'SKILL.md')))
|
|
71
|
+
.map((d) => parseSkill(path.join(SKILLS_DIR, d.name)))
|
|
72
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = { listSkills, parseSkill, parseFrontmatter, SKILLS_DIR };
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@smitdev/ai-skills",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Install reusable AI assistant skills (Claude Code, GitHub Copilot, Cursor, Windsurf) with one command.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"ai-skills": "bin/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"type": "commonjs",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node --test"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin/",
|
|
14
|
+
"lib/",
|
|
15
|
+
"skills/",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"claude",
|
|
21
|
+
"claude-code",
|
|
22
|
+
"copilot",
|
|
23
|
+
"cursor",
|
|
24
|
+
"windsurf",
|
|
25
|
+
"ai",
|
|
26
|
+
"skills",
|
|
27
|
+
"agent",
|
|
28
|
+
"prompt"
|
|
29
|
+
],
|
|
30
|
+
"author": "smitdev",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/smitcode/ai-skills.git"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/smitcode/ai-skills#readme",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/smitcode/ai-skills/issues"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: contract
|
|
3
|
+
description: Create a contract - a spec or PRD that fully explains a feature before any code is written, so a coding assistant or engineer can build it without guessing. Use this whenever the user wants to plan or spec a feature before building - things like "spec this out", "write a PRD", "let's plan this feature first", "make an implementation doc", "write the contract for X", or "I want a plan before we touch the code". Trigger it even if the user doesn't say the word "contract" - any time they want a build-ready description of a feature instead of the code itself.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Contract
|
|
7
|
+
|
|
8
|
+
A contract is a plan that you and the user agree on before any code is written. It is the one document a coding assistant reads to build the feature without guessing or making things up. Your job here is to make that document - not to write the feature code.
|
|
9
|
+
|
|
10
|
+
Do these three steps in order: understand the codebase, then ask the user questions, then write the contract. Don't jump straight to writing - a contract built on guesses is worse than no contract.
|
|
11
|
+
|
|
12
|
+
## Step 1 - Understand the codebase
|
|
13
|
+
|
|
14
|
+
Before you ask the user anything, read the parts of the codebase the feature will touch. You want real files, not a vague idea of the project.
|
|
15
|
+
|
|
16
|
+
Find and read the files that will likely change, or that the new code has to work with. For each one, note what it does now, the patterns it follows (naming, error handling, how state and APIs are done), and where the new feature would fit in.
|
|
17
|
+
|
|
18
|
+
By the end of this step you should have a short list of real file paths, what each one does today, and where the new feature connects. You'll use this directly in the contract, so write down the real paths - never write a spec that isn't connected to the real code.
|
|
19
|
+
|
|
20
|
+
## Step 2 - Ask the user questions
|
|
21
|
+
|
|
22
|
+
Now fill in the gaps. The user knows what they want; you know the code. The questions bring the two together and find the decisions that shape the design.
|
|
23
|
+
|
|
24
|
+
Ask one question at a time - never a big list at once. After each answer, ask the next one. This keeps it easy for the user and lets each answer guide the next question.
|
|
25
|
+
|
|
26
|
+
For each question, give a short list of simple answer options to pick from, not an open question. Keep the options non-technical - describe what happens, not how it's built. Mark the option you recommend and say in one line why you'd pick it, then ask if they're okay with it. Always add an "other - let me explain" option so they're not stuck with your choices.
|
|
27
|
+
|
|
28
|
+
Across the questions, cover: what the feature should do for the user, what's not included (the limits of the work), edge cases and error states, and any spot where two reasonable choices exist. Stop once you can explain the whole feature start to finish with no "it depends" left.
|
|
29
|
+
|
|
30
|
+
**Example of how a question should look:**
|
|
31
|
+
|
|
32
|
+
> When a search returns no results, what should the user see?
|
|
33
|
+
>
|
|
34
|
+
> - **A - A short "nothing found" message** (recommended: simplest, and it matches how the rest of the app handles empty states)
|
|
35
|
+
> - **B - The empty state plus a few suggestions on what to try next**
|
|
36
|
+
> - **C - Keep the last results on screen and just show a small note**
|
|
37
|
+
> - **D - Something else - tell me what you have in mind**
|
|
38
|
+
>
|
|
39
|
+
> I'd go with A. Want to go with that, or pick another?
|
|
40
|
+
|
|
41
|
+
## Step 3 - Check, then write the contract
|
|
42
|
+
|
|
43
|
+
Before you write the full document, say your understanding back in a few sentences and get a yes. This is a cheap way to avoid writing a long doc based on a wrong idea.
|
|
44
|
+
|
|
45
|
+
Then write the contract using the template below. Leave out any section that doesn't apply (for example, no "Data model" section for a simple styling change) instead of filling it with fluff - but keep the numbers on the sections you do use. Write it so that an engineer or coding assistant who never saw the conversation could build the feature correctly from this document alone.
|
|
46
|
+
|
|
47
|
+
### Contract template
|
|
48
|
+
|
|
49
|
+
```markdown
|
|
50
|
+
# Contract: <Feature name>
|
|
51
|
+
|
|
52
|
+
**Status:** Draft
|
|
53
|
+
**Date:** <YYYY-MM-DD>
|
|
54
|
+
**Area:** <which part of the product / module>
|
|
55
|
+
|
|
56
|
+
## 1. Summary
|
|
57
|
+
|
|
58
|
+
One short paragraph, plain words: what we're building and why.
|
|
59
|
+
|
|
60
|
+
## 2. Problem
|
|
61
|
+
|
|
62
|
+
The problem this solves. What happens now and why that's not good enough.
|
|
63
|
+
|
|
64
|
+
## 3. Goals
|
|
65
|
+
|
|
66
|
+
Clear bullets - what success looks like.
|
|
67
|
+
|
|
68
|
+
## 4. Not included (out of scope)
|
|
69
|
+
|
|
70
|
+
What this feature does NOT do. This is just as important as the goals - it
|
|
71
|
+
stops a coding assistant from adding extra work or features no one asked for.
|
|
72
|
+
|
|
73
|
+
## 5. The code today (what's there now)
|
|
74
|
+
|
|
75
|
+
The real files involved and what they do now:
|
|
76
|
+
|
|
77
|
+
- `path/to/file.ts` - what it does today; what changes here
|
|
78
|
+
- `path/to/other.py` - ...
|
|
79
|
+
Patterns and conventions the new code should follow.
|
|
80
|
+
|
|
81
|
+
## 6. The plan
|
|
82
|
+
|
|
83
|
+
The approach in a sentence or two, then broken down:
|
|
84
|
+
|
|
85
|
+
### 6.1 UI and UX
|
|
86
|
+
|
|
87
|
+
What the user sees and does. Every state: normal, loading, empty, error,
|
|
88
|
+
success. Note accessibility where it matters.
|
|
89
|
+
|
|
90
|
+
### 6.2 Data model
|
|
91
|
+
|
|
92
|
+
New or changed data: entities, fields, types, how they relate. (Skip if none.)
|
|
93
|
+
|
|
94
|
+
### 6.3 API / interface
|
|
95
|
+
|
|
96
|
+
For each endpoint or function: the signature or method + path, what goes in,
|
|
97
|
+
what comes back, and the error cases. For frontend: component props, events,
|
|
98
|
+
and the state it gives back.
|
|
99
|
+
|
|
100
|
+
### 6.4 Logic and edge cases
|
|
101
|
+
|
|
102
|
+
The step-by-step behavior. List the edge cases and say how each is handled -
|
|
103
|
+
this is where most of the confusion hides.
|
|
104
|
+
|
|
105
|
+
## 7. Performance and optimization
|
|
106
|
+
|
|
107
|
+
How fast and how cheap this has to be, made concrete (real numbers where you
|
|
108
|
+
can). Cover what applies:
|
|
109
|
+
|
|
110
|
+
- Expected load / scale - how many requests, rows, items, or users, etc.
|
|
111
|
+
- The hot paths - the parts that run a lot or work on big data, and the target
|
|
112
|
+
(e.g. "the list should load in under X seconds with 10,000 rows").
|
|
113
|
+
- Caching, batching, rate limits, concurrency - what to reuse or throttle.
|
|
114
|
+
- Cost - any API quota or money limits, and how to stay under them.
|
|
115
|
+
Skip this only if the feature has no real performance concern.
|
|
116
|
+
|
|
117
|
+
## 8. Other requirements
|
|
118
|
+
|
|
119
|
+
Only the ones that apply: security and access, logging/monitoring, easy to
|
|
120
|
+
maintain, reusable.
|
|
121
|
+
|
|
122
|
+
## 9. Files to change (checklist)
|
|
123
|
+
|
|
124
|
+
A checklist the coding assistant can follow:
|
|
125
|
+
|
|
126
|
+
- [ ] `path/to/file.ts` - what to change
|
|
127
|
+
- [ ] `path/to/new_file.py` - new; what it holds
|
|
128
|
+
|
|
129
|
+
## 10. Done when (acceptance criteria)
|
|
130
|
+
|
|
131
|
+
How we know it's done, written as things you can test:
|
|
132
|
+
|
|
133
|
+
- Given <situation>, when <action>, then <result>.
|
|
134
|
+
|
|
135
|
+
## 11. Risks and open questions
|
|
136
|
+
|
|
137
|
+
Guesses you made, choices left for later, anything still unclear.
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Where to save it
|
|
141
|
+
|
|
142
|
+
Save the contract as a Markdown file. Use the project's existing folder if there is one - look for a `specs/`, `contracts/`, `docs/specs/`, or `.specify/` folder and put it there. If there isn't one, make a `specs/` folder.
|
|
143
|
+
|
|
144
|
+
Name the file after the feature, in kebab-case: `specs/<feature-slug>.md` (for example `specs/user-profile-page.md`). Tell the user the path when you're done.
|
|
145
|
+
|
|
146
|
+
## Rules to keep in mind
|
|
147
|
+
|
|
148
|
+
- Make the contract, not the code. If the user also wants the code, finish and confirm the contract first - that's the thing they'll read and reuse.
|
|
149
|
+
- Keep the user-facing parts (Summary, Problem, Goals) in plain words, and the build-facing parts (API, done-when) exact. The coding assistant needs the exact parts to be exact.
|
|
150
|
+
- If you hit a decision while writing that you didn't settle in the questions, stop and ask instead of guessing - then write the answer into the doc.
|