claude-agent-skills 1.3.4 → 1.3.6
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/lib/picker.js +172 -0
- package/lib/prompts.js +12 -17
- package/package.json +1 -1
- package/skills.json +1 -1
package/lib/picker.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom skill picker — grid layout, full description panel for focused skill.
|
|
3
|
+
* Navigation: ↑↓←→ space=toggle a=all enter=confirm esc=back
|
|
4
|
+
*/
|
|
5
|
+
import ansis from 'ansis';
|
|
6
|
+
import { skillColor, white, muted, success, brand, warn } from './theme.js';
|
|
7
|
+
|
|
8
|
+
const KEY = {
|
|
9
|
+
UP: '\x1b[A',
|
|
10
|
+
DOWN: '\x1b[B',
|
|
11
|
+
RIGHT: '\x1b[C',
|
|
12
|
+
LEFT: '\x1b[D',
|
|
13
|
+
SPACE: ' ',
|
|
14
|
+
ENTER: '\r',
|
|
15
|
+
CTRL_C: '\x03',
|
|
16
|
+
ESC: '\x1b',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function wordWrap(text, width) {
|
|
20
|
+
const words = text.split(' ');
|
|
21
|
+
const lines = [];
|
|
22
|
+
let cur = '';
|
|
23
|
+
for (const w of words) {
|
|
24
|
+
if (cur && cur.length + 1 + w.length > width) { lines.push(cur); cur = w; }
|
|
25
|
+
else cur = cur ? `${cur} ${w}` : w;
|
|
26
|
+
}
|
|
27
|
+
if (cur) lines.push(cur);
|
|
28
|
+
return lines;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function skillPicker({ message, options }) {
|
|
32
|
+
const termW = process.stdout.columns || 100;
|
|
33
|
+
const termH = process.stdout.rows || 40;
|
|
34
|
+
|
|
35
|
+
// Grid dimensions
|
|
36
|
+
const maxName = Math.max(...options.map(o => o.label.length));
|
|
37
|
+
const colW = maxName + 6; // □ name + padding
|
|
38
|
+
const numCols = Math.max(1, Math.floor((termW - 2) / colW));
|
|
39
|
+
const numRows = Math.ceil(options.length / numCols);
|
|
40
|
+
|
|
41
|
+
// Fixed layout heights
|
|
42
|
+
const HEADER = 2;
|
|
43
|
+
const GRID_H = numRows;
|
|
44
|
+
const SEP = 1;
|
|
45
|
+
const DESC_H = Math.max(4, Math.min(8, termH - HEADER - GRID_H - SEP - 3));
|
|
46
|
+
const FOOTER = 1;
|
|
47
|
+
const TOTAL = HEADER + GRID_H + SEP + DESC_H + FOOTER;
|
|
48
|
+
|
|
49
|
+
let cursor = 0;
|
|
50
|
+
const sel = new Set();
|
|
51
|
+
let lastLines = 0;
|
|
52
|
+
|
|
53
|
+
function clamp(c) {
|
|
54
|
+
return Math.max(0, Math.min(options.length - 1, c));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function renderGrid() {
|
|
58
|
+
const lines = [];
|
|
59
|
+
|
|
60
|
+
// Header
|
|
61
|
+
lines.push(brand('◆') + ' ' + ansis.bold(white(message)) +
|
|
62
|
+
muted(' ↑↓←→ navigate · space toggle · a=all · enter confirm · esc back'));
|
|
63
|
+
lines.push(muted('│'));
|
|
64
|
+
|
|
65
|
+
// Grid rows
|
|
66
|
+
for (let r = 0; r < numRows; r++) {
|
|
67
|
+
let row = muted('│') + ' ';
|
|
68
|
+
for (let c = 0; c < numCols; c++) {
|
|
69
|
+
const idx = r * numCols + c;
|
|
70
|
+
if (idx >= options.length) break;
|
|
71
|
+
const opt = options[idx];
|
|
72
|
+
const focused = idx === cursor;
|
|
73
|
+
const checked = sel.has(idx);
|
|
74
|
+
const box = checked ? success('■') : muted('□');
|
|
75
|
+
const arrow = focused ? brand('▶') : ' ';
|
|
76
|
+
const label = focused
|
|
77
|
+
? ansis.bold(skillColor(idx)(opt.label))
|
|
78
|
+
: skillColor(idx)(opt.label);
|
|
79
|
+
const cell = `${arrow}${box} ${label}`;
|
|
80
|
+
// Pad to colW (accounting for invisible ANSI chars: pad by name length, not cell length)
|
|
81
|
+
const visLen = 3 + opt.label.length; // arrow(1) + box(1) + space(1) + name
|
|
82
|
+
const pad = Math.max(0, colW - visLen);
|
|
83
|
+
row += cell + ' '.repeat(pad);
|
|
84
|
+
}
|
|
85
|
+
lines.push(row);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Separator + description
|
|
89
|
+
lines.push(muted('├' + '─'.repeat(Math.min(termW - 2, 78)) + '┤'));
|
|
90
|
+
|
|
91
|
+
const focused = options[cursor];
|
|
92
|
+
const desc = focused?.description ?? '';
|
|
93
|
+
const wrapped = desc ? wordWrap(desc, termW - 6) : [];
|
|
94
|
+
for (let i = 0; i < DESC_H; i++) {
|
|
95
|
+
const line = wrapped[i] ?? '';
|
|
96
|
+
lines.push(muted('│ ') + white(line));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Footer
|
|
100
|
+
const selCount = sel.size;
|
|
101
|
+
lines.push(
|
|
102
|
+
' ' + (selCount > 0 ? success(`${selCount} selected`) : muted('0 selected')) +
|
|
103
|
+
muted(` · ${cursor + 1} of ${options.length}`)
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
return lines;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function draw() {
|
|
110
|
+
const lines = renderGrid();
|
|
111
|
+
// Go up by however many lines we drew last time
|
|
112
|
+
if (lastLines > 0) {
|
|
113
|
+
process.stdout.write(`\x1b[${lastLines}A`);
|
|
114
|
+
}
|
|
115
|
+
for (const line of lines) {
|
|
116
|
+
process.stdout.write('\x1b[2K' + line + '\n');
|
|
117
|
+
}
|
|
118
|
+
lastLines = lines.length;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Initial draw
|
|
122
|
+
draw();
|
|
123
|
+
|
|
124
|
+
return new Promise((resolve, reject) => {
|
|
125
|
+
process.stdin.setRawMode(true);
|
|
126
|
+
process.stdin.resume();
|
|
127
|
+
process.stdin.setEncoding('utf8');
|
|
128
|
+
|
|
129
|
+
function cleanup() {
|
|
130
|
+
process.stdin.setRawMode(false);
|
|
131
|
+
process.stdin.pause();
|
|
132
|
+
process.stdin.removeAllListeners('data');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function cancel() {
|
|
136
|
+
cleanup();
|
|
137
|
+
reject(Object.assign(new Error('cancel'), { isCancel: true }));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
process.stdin.on('data', key => {
|
|
141
|
+
const row = Math.floor(cursor / numCols);
|
|
142
|
+
const col = cursor % numCols;
|
|
143
|
+
|
|
144
|
+
if (key === KEY.CTRL_C) { cleanup(); process.exit(0); }
|
|
145
|
+
if (key === KEY.ESC) { cancel(); return; }
|
|
146
|
+
|
|
147
|
+
if (key === KEY.UP) cursor = clamp((row - 1) * numCols + col);
|
|
148
|
+
if (key === KEY.DOWN) cursor = clamp((row + 1) * numCols + col);
|
|
149
|
+
if (key === KEY.LEFT) cursor = clamp(cursor - 1);
|
|
150
|
+
if (key === KEY.RIGHT) cursor = clamp(cursor + 1);
|
|
151
|
+
|
|
152
|
+
if (key === KEY.SPACE) {
|
|
153
|
+
sel.has(cursor) ? sel.delete(cursor) : sel.add(cursor);
|
|
154
|
+
}
|
|
155
|
+
if (key === 'a' || key === 'A') {
|
|
156
|
+
sel.size === options.length
|
|
157
|
+
? sel.clear()
|
|
158
|
+
: options.forEach((_, i) => sel.add(i));
|
|
159
|
+
}
|
|
160
|
+
if (key === KEY.ENTER) {
|
|
161
|
+
if (!sel.size) return;
|
|
162
|
+
cleanup();
|
|
163
|
+
process.stdout.write('\x1b[2K' + success('◆') + ' ' +
|
|
164
|
+
white(`${sel.size} skill(s) selected`) + '\n');
|
|
165
|
+
resolve([...sel].sort((a, b) => a - b).map(i => options[i].value));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
draw();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
}
|
package/lib/prompts.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { select,
|
|
1
|
+
import { select, confirm, isCancel } from '@clack/prompts';
|
|
2
2
|
import ansis from 'ansis';
|
|
3
3
|
import { skillColor, white, muted, divider } from './theme.js';
|
|
4
|
+
import { skillPicker } from './picker.js';
|
|
4
5
|
|
|
5
6
|
export class CliCancel extends Error {}
|
|
6
7
|
|
|
@@ -28,23 +29,17 @@ export async function pickSkills(names, flags = {}, descriptions = {}) {
|
|
|
28
29
|
if (flags.all) return names;
|
|
29
30
|
if (flags.skill?.length) return flags.skill;
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
try {
|
|
33
|
+
const result = await skillPicker({
|
|
34
|
+
message: 'Select skills',
|
|
35
|
+
options: names.map(n => ({ value: n, label: n, description: descriptions[n] || '' })),
|
|
36
|
+
});
|
|
37
|
+
if (result === null) throw new CliCancel();
|
|
38
|
+
return result;
|
|
39
|
+
} catch (e) {
|
|
40
|
+
if (e?.isCancel) throw new CliCancel();
|
|
41
|
+
throw e;
|
|
37
42
|
}
|
|
38
|
-
|
|
39
|
-
return guard(await multiselect({
|
|
40
|
-
message: 'Select skills (space to toggle, a for all, enter to confirm)',
|
|
41
|
-
options: names.map((n, i) => {
|
|
42
|
-
const name = skillColor(i)(ansis.bold(n.padEnd(32)));
|
|
43
|
-
const desc = descriptions[n] ? white(fitDesc(descriptions[n])) : '';
|
|
44
|
-
return { value: n, label: name + divider + desc };
|
|
45
|
-
}),
|
|
46
|
-
required: true,
|
|
47
|
-
}));
|
|
48
43
|
}
|
|
49
44
|
|
|
50
45
|
export async function pickLinkType(flags = {}) {
|
package/package.json
CHANGED