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 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, multiselect, confirm, isCancel } from '@clack/prompts';
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
- // Hard cap so descriptions never wrap — single line always fits ≥80 col terminals.
32
- // prefix(4) + name(32) + divider(3) + desc(45) = 84 chars total.
33
- const DESC_MAX = 45;
34
- function fitDesc(s) {
35
- if (!s) return '';
36
- return s.length <= DESC_MAX ? s : s.slice(0, DESC_MAX - 1) + '…';
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-agent-skills",
3
- "version": "1.3.4",
3
+ "version": "1.3.6",
4
4
  "description": "Install and manage Pavi's Claude Code agent skills",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/skills.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema_version": 1,
3
3
  "name": "claude-agent-skills",
4
- "version": "1.3.4",
4
+ "version": "1.3.6",
5
5
  "skills": [
6
6
  "ask-matt",
7
7
  "brainstorming",