@smitdev/ai-skills 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/bin/cli.js +30 -34
- package/lib/prompt.js +185 -0
- package/package.json +1 -1
- package/skills/understand/SKILL.md +46 -1
package/README.md
CHANGED
|
@@ -14,7 +14,8 @@ No global install needed — run it with `npx`:
|
|
|
14
14
|
# See what's available
|
|
15
15
|
npx @smitdev/ai-skills list
|
|
16
16
|
|
|
17
|
-
# Interactive: pick assistant(s) and skill(s)
|
|
17
|
+
# Interactive: pick assistant(s) and skill(s) with the keyboard
|
|
18
|
+
# ↑/↓ move · space or tab toggle · a select all · enter confirm
|
|
18
19
|
npx @smitdev/ai-skills install
|
|
19
20
|
|
|
20
21
|
# Non-interactive examples
|
package/bin/cli.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
const readline = require('readline');
|
|
5
4
|
const path = require('path');
|
|
6
5
|
const { listSkills } = require('../lib/skills');
|
|
7
6
|
const { adapters } = require('../lib/adapters');
|
|
8
7
|
const { install } = require('../lib/install');
|
|
8
|
+
const { multiselect, select } = require('../lib/prompt');
|
|
9
9
|
|
|
10
10
|
const ASSISTANT_IDS = Object.keys(adapters);
|
|
11
11
|
|
|
@@ -31,10 +31,6 @@ function parseArgs(argv) {
|
|
|
31
31
|
return args;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
function ask(rl, question) {
|
|
35
|
-
return new Promise((resolve) => rl.question(question, (a) => resolve(a.trim())));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
34
|
function parseList(val, valid) {
|
|
39
35
|
if (val === true || !val) return [];
|
|
40
36
|
const items = String(val)
|
|
@@ -55,6 +51,8 @@ Usage:
|
|
|
55
51
|
Commands:
|
|
56
52
|
list List the skills bundled in this package
|
|
57
53
|
install Install skills for one or more assistants
|
|
54
|
+
(run with no flags for an interactive picker:
|
|
55
|
+
↑/↓ move · space/tab toggle · a all · enter confirm)
|
|
58
56
|
|
|
59
57
|
Options for "install":
|
|
60
58
|
--assistant <ids> Comma-separated: ${ASSISTANT_IDS.join(', ')} (or "all")
|
|
@@ -73,20 +71,6 @@ Examples:
|
|
|
73
71
|
`);
|
|
74
72
|
}
|
|
75
73
|
|
|
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
74
|
async function runInstall(args) {
|
|
91
75
|
const skills = listSkills();
|
|
92
76
|
if (skills.length === 0) {
|
|
@@ -98,31 +82,43 @@ async function runInstall(args) {
|
|
|
98
82
|
typeof args.flags.dir === 'string' ? args.flags.dir : process.cwd()
|
|
99
83
|
);
|
|
100
84
|
const dryRun = !!args.flags['dry-run'];
|
|
101
|
-
const
|
|
85
|
+
const scopeFlagged = !!args.flags.global || !!args.flags.project;
|
|
86
|
+
let scope = args.flags.global ? 'global' : 'project';
|
|
102
87
|
|
|
103
88
|
let assistants = parseList(args.flags.assistant, ASSISTANT_IDS);
|
|
104
89
|
let skillNames = parseList(args.flags.skill, skills.map((s) => s.name));
|
|
105
90
|
|
|
106
|
-
const nonInteractive =
|
|
107
|
-
args.flags.yes || (assistants.length > 0);
|
|
91
|
+
const nonInteractive = args.flags.yes || assistants.length > 0;
|
|
108
92
|
|
|
109
93
|
if (!nonInteractive && process.stdin.isTTY) {
|
|
110
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
111
94
|
if (assistants.length === 0) {
|
|
112
|
-
assistants = await
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
);
|
|
95
|
+
assistants = await multiselect({
|
|
96
|
+
title: 'Which assistant(s)? (Claude Code, Copilot, Cursor, Windsurf)',
|
|
97
|
+
options: ASSISTANT_IDS.map((id) => ({ id, label: adapters[id].label })),
|
|
98
|
+
initial: ['claude'],
|
|
99
|
+
});
|
|
117
100
|
}
|
|
118
101
|
if (skillNames.length === 0) {
|
|
119
|
-
skillNames = await
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
102
|
+
skillNames = await multiselect({
|
|
103
|
+
title: 'Which skill(s)?',
|
|
104
|
+
options: skills.map((s) => ({
|
|
105
|
+
id: s.name,
|
|
106
|
+
label: `${s.name} — ${truncate(s.description, 60)}`,
|
|
107
|
+
})),
|
|
108
|
+
initial: skills.map((s) => s.name),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
// Only Claude Code supports a global scope; ask when relevant.
|
|
112
|
+
if (!scopeFlagged && assistants.includes('claude')) {
|
|
113
|
+
scope = await select({
|
|
114
|
+
title: 'Install scope for Claude Code?',
|
|
115
|
+
options: [
|
|
116
|
+
{ id: 'project', label: 'This project (./.claude/skills)' },
|
|
117
|
+
{ id: 'global', label: 'Global (~/.claude/skills, available everywhere)' },
|
|
118
|
+
],
|
|
119
|
+
initial: 'project',
|
|
120
|
+
});
|
|
124
121
|
}
|
|
125
|
-
rl.close();
|
|
126
122
|
}
|
|
127
123
|
|
|
128
124
|
if (assistants.length === 0) assistants = ASSISTANT_IDS.slice();
|
package/lib/prompt.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const readline = require('readline');
|
|
4
|
+
|
|
5
|
+
// Tiny zero-dependency interactive prompts (checkbox + radio) built on
|
|
6
|
+
// readline keypress events. Falls back to nothing in non-TTY environments —
|
|
7
|
+
// callers should guard with process.stdin.isTTY.
|
|
8
|
+
|
|
9
|
+
const useColor = !!process.stdout.isTTY && !process.env.NO_COLOR;
|
|
10
|
+
const paint = (code) => (s) => (useColor ? `\x1b[${code}m${s}\x1b[0m` : String(s));
|
|
11
|
+
const dim = paint('2');
|
|
12
|
+
const bold = paint('1');
|
|
13
|
+
const cyan = paint('36');
|
|
14
|
+
const green = paint('32');
|
|
15
|
+
|
|
16
|
+
function setupKeys() {
|
|
17
|
+
const input = process.stdin;
|
|
18
|
+
readline.emitKeypressEvents(input);
|
|
19
|
+
if (input.isTTY) input.setRawMode(true);
|
|
20
|
+
input.resume();
|
|
21
|
+
return input;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function teardownKeys(input, onKey) {
|
|
25
|
+
input.removeListener('keypress', onKey);
|
|
26
|
+
if (input.isTTY) input.setRawMode(false);
|
|
27
|
+
input.pause();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function hideCursor() {
|
|
31
|
+
if (useColor) process.stdout.write('\x1b[?25l');
|
|
32
|
+
}
|
|
33
|
+
function showCursor() {
|
|
34
|
+
if (useColor) process.stdout.write('\x1b[?25h');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Multi-select checkbox prompt.
|
|
39
|
+
* options: [{ id, label }]
|
|
40
|
+
* initial: array of pre-selected ids
|
|
41
|
+
* Resolves to an array of selected ids (in option order). Always returns at
|
|
42
|
+
* least one (the highlighted item) so callers never get an empty selection.
|
|
43
|
+
*/
|
|
44
|
+
function multiselect({ title, options, initial }) {
|
|
45
|
+
return new Promise((resolve) => {
|
|
46
|
+
const selected = new Set(initial || []);
|
|
47
|
+
let cursor = 0;
|
|
48
|
+
let rendered = 0;
|
|
49
|
+
const output = process.stdout;
|
|
50
|
+
|
|
51
|
+
const hint = dim(
|
|
52
|
+
'↑/↓ move · space/tab toggle · a all · enter confirm'
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
function render(first) {
|
|
56
|
+
if (!first) output.write(`\x1b[${rendered}A`);
|
|
57
|
+
const lines = [bold(title), hint];
|
|
58
|
+
options.forEach((o, i) => {
|
|
59
|
+
const active = i === cursor;
|
|
60
|
+
const pointer = active ? cyan('›') : ' ';
|
|
61
|
+
const box = selected.has(o.id) ? green('◉') : dim('◯');
|
|
62
|
+
const label = active ? cyan(o.label) : o.label;
|
|
63
|
+
lines.push(`${pointer} ${box} ${label}`);
|
|
64
|
+
});
|
|
65
|
+
output.write(lines.map((l) => '\x1b[2K' + l).join('\n') + '\n');
|
|
66
|
+
rendered = lines.length;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const input = setupKeys();
|
|
70
|
+
hideCursor();
|
|
71
|
+
|
|
72
|
+
function finish() {
|
|
73
|
+
if (selected.size === 0) selected.add(options[cursor].id);
|
|
74
|
+
teardownKeys(input, onKey);
|
|
75
|
+
showCursor();
|
|
76
|
+
resolve(options.filter((o) => selected.has(o.id)).map((o) => o.id));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function onKey(_str, key) {
|
|
80
|
+
if (!key) return;
|
|
81
|
+
if (key.ctrl && key.name === 'c') {
|
|
82
|
+
teardownKeys(input, onKey);
|
|
83
|
+
showCursor();
|
|
84
|
+
output.write('\n');
|
|
85
|
+
process.exit(130);
|
|
86
|
+
}
|
|
87
|
+
switch (key.name) {
|
|
88
|
+
case 'up':
|
|
89
|
+
case 'k':
|
|
90
|
+
cursor = (cursor - 1 + options.length) % options.length;
|
|
91
|
+
break;
|
|
92
|
+
case 'down':
|
|
93
|
+
case 'j':
|
|
94
|
+
cursor = (cursor + 1) % options.length;
|
|
95
|
+
break;
|
|
96
|
+
case 'space':
|
|
97
|
+
case 'tab':
|
|
98
|
+
if (selected.has(options[cursor].id)) selected.delete(options[cursor].id);
|
|
99
|
+
else selected.add(options[cursor].id);
|
|
100
|
+
break;
|
|
101
|
+
case 'a': {
|
|
102
|
+
if (selected.size === options.length) selected.clear();
|
|
103
|
+
else options.forEach((o) => selected.add(o.id));
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
case 'return':
|
|
107
|
+
case 'enter':
|
|
108
|
+
finish();
|
|
109
|
+
return;
|
|
110
|
+
default:
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
render(false);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
input.on('keypress', onKey);
|
|
117
|
+
render(true);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Single-select radio prompt.
|
|
123
|
+
* options: [{ id, label }]
|
|
124
|
+
* Resolves to one id.
|
|
125
|
+
*/
|
|
126
|
+
function select({ title, options, initial }) {
|
|
127
|
+
return new Promise((resolve) => {
|
|
128
|
+
let cursor = Math.max(0, options.findIndex((o) => o.id === initial));
|
|
129
|
+
if (cursor < 0) cursor = 0;
|
|
130
|
+
let rendered = 0;
|
|
131
|
+
const output = process.stdout;
|
|
132
|
+
const hint = dim('↑/↓ move · enter select');
|
|
133
|
+
|
|
134
|
+
function render(first) {
|
|
135
|
+
if (!first) output.write(`\x1b[${rendered}A`);
|
|
136
|
+
const lines = [bold(title), hint];
|
|
137
|
+
options.forEach((o, i) => {
|
|
138
|
+
const active = i === cursor;
|
|
139
|
+
const pointer = active ? cyan('›') : ' ';
|
|
140
|
+
const dot = active ? green('◉') : dim('◯');
|
|
141
|
+
const label = active ? cyan(o.label) : o.label;
|
|
142
|
+
lines.push(`${pointer} ${dot} ${label}`);
|
|
143
|
+
});
|
|
144
|
+
output.write(lines.map((l) => '\x1b[2K' + l).join('\n') + '\n');
|
|
145
|
+
rendered = lines.length;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const input = setupKeys();
|
|
149
|
+
hideCursor();
|
|
150
|
+
|
|
151
|
+
function onKey(_str, key) {
|
|
152
|
+
if (!key) return;
|
|
153
|
+
if (key.ctrl && key.name === 'c') {
|
|
154
|
+
teardownKeys(input, onKey);
|
|
155
|
+
showCursor();
|
|
156
|
+
output.write('\n');
|
|
157
|
+
process.exit(130);
|
|
158
|
+
}
|
|
159
|
+
switch (key.name) {
|
|
160
|
+
case 'up':
|
|
161
|
+
case 'k':
|
|
162
|
+
cursor = (cursor - 1 + options.length) % options.length;
|
|
163
|
+
break;
|
|
164
|
+
case 'down':
|
|
165
|
+
case 'j':
|
|
166
|
+
cursor = (cursor + 1) % options.length;
|
|
167
|
+
break;
|
|
168
|
+
case 'return':
|
|
169
|
+
case 'enter':
|
|
170
|
+
teardownKeys(input, onKey);
|
|
171
|
+
showCursor();
|
|
172
|
+
resolve(options[cursor].id);
|
|
173
|
+
return;
|
|
174
|
+
default:
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
render(false);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
input.on('keypress', onKey);
|
|
181
|
+
render(true);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = { multiselect, select };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: understand
|
|
3
|
-
description: Read a whole codebase and write a set of plain-language docs that explain it - one overview/map doc plus one doc per aspect (UI, styles, API, models, data, testing, features, services, and so on). Use this when someone wants to understand a project as a whole or get onboarded to a repo - things like "help me understand this codebase", "document this repo", "explain how this project is structured", "map out the code", "onboard me to this project", or "I just joined and need to learn this code". This is for the WHOLE codebase.
|
|
3
|
+
description: Read a whole codebase and write a set of plain-language docs that explain it - one overview/map doc plus one doc per aspect (UI, styles, API, models, data, testing, features, services, and so on). Use this when someone wants to understand a project as a whole or get onboarded to a repo - things like "help me understand this codebase", "document this repo", "explain how this project is structured", "map out the code", "onboard me to this project", or "I just joined and need to learn this code". This is for the WHOLE codebase. It can also explain one single feature in depth on request - things like "explain how login works", "walk me through the checkout flow", "how does feature X work" - but only after confirming with the user first (see "Explaining a single feature on request").
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Understanding
|
|
@@ -124,6 +124,51 @@ Put all the docs in one folder so they're a set. Reuse the project's docs folder
|
|
|
124
124
|
|
|
125
125
|
Name the files simply: the map is `overview.md`, and each aspect is its lowercase name (`api.md`, `ui.md`, `data.md`, ...). Tell the user the folder path and the list of files when you're done.
|
|
126
126
|
|
|
127
|
+
## Explaining a single feature on request
|
|
128
|
+
|
|
129
|
+
Sometimes the user doesn't want the whole codebase mapped - they want one feature explained in depth (e.g. "explain how login works", "walk me through the checkout flow", "how does search work"). This skill can do that too, but it's a different job from the full map, so **always confirm before starting**.
|
|
130
|
+
|
|
131
|
+
When a request looks like a single-feature explanation:
|
|
132
|
+
|
|
133
|
+
1. **Confirm first.** Don't dive in. Ask the user something like: "Do you want me to explain just the `<feature>` feature in depth, or map the whole codebase? And should I write it to a doc file or just explain it here in the chat?" Wait for their answer before doing the work. Only proceed once they've confirmed the scope (one feature vs. whole repo) and the output (a file vs. inline).
|
|
134
|
+
|
|
135
|
+
2. **Trace the feature, not the repo.** Once confirmed, follow the one feature end to end instead of surveying everything. Find its entry point (the button, route, command, or event that kicks it off) and follow it through the layers - UI -> API -> services -> data and back. Read only the files on that path.
|
|
136
|
+
|
|
137
|
+
3. **Write the explanation** using this shape:
|
|
138
|
+
|
|
139
|
+
```markdown
|
|
140
|
+
# Feature: <feature name>
|
|
141
|
+
|
|
142
|
+
## What it does
|
|
143
|
+
|
|
144
|
+
One short paragraph in plain words - what this feature is, from the user's point of view.
|
|
145
|
+
|
|
146
|
+
## How it's triggered
|
|
147
|
+
|
|
148
|
+
Where it starts - the button, route, command, or event that kicks it off, with the real file path.
|
|
149
|
+
|
|
150
|
+
## How it flows end to end
|
|
151
|
+
|
|
152
|
+
Follow the feature step by step through the layers (UI -> API -> service -> data
|
|
153
|
+
-> back). This is the core of the doc. A numbered list or a small diagram works well.
|
|
154
|
+
Point to the real file (and function) at each step.
|
|
155
|
+
|
|
156
|
+
## The main pieces
|
|
157
|
+
|
|
158
|
+
The key files/functions involved and what each one contributes. Just the ones on this
|
|
159
|
+
feature's path, not the whole repo.
|
|
160
|
+
|
|
161
|
+
## Edge cases and gotchas
|
|
162
|
+
|
|
163
|
+
Error handling, validation, special states, and the non-obvious bits someone would miss.
|
|
164
|
+
|
|
165
|
+
## Where to start reading
|
|
166
|
+
|
|
167
|
+
The first file or two to open to follow this feature.
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
4. **Output where they asked.** If they wanted a file, save it in the same docs folder the full maps use (see "Where to save the docs") as `feature-<name>.md`. If they wanted it inline, just explain it in the chat. Either way, follow the same rules below - real file paths, no invention.
|
|
171
|
+
|
|
127
172
|
## Rules to keep in mind
|
|
128
173
|
|
|
129
174
|
- Write for a newcomer. Plain words, real examples, no walls of jargon.
|