claude-agent-skills 1.3.5 → 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 CHANGED
@@ -1,146 +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
+ */
1
5
  import ansis from 'ansis';
2
- import { skillColor, white, muted, success, brand } from './theme.js';
3
-
4
- const UP = '\x1b[A';
5
- const DOWN = '\x1b[B';
6
- const SPACE = ' ';
7
- const ENTER = '\r';
8
- const CTRL_C = '\x03';
9
- const ESC = '\x1b';
10
-
11
- const clr = () => process.stdout.write('\x1b[2K');
12
- const up = n => process.stdout.write(`\x1b[${n}A`);
13
- const nl = () => process.stdout.write('\n');
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
+ };
14
18
 
15
19
  function wordWrap(text, width) {
16
20
  const words = text.split(' ');
17
21
  const lines = [];
18
- let line = '';
22
+ let cur = '';
19
23
  for (const w of words) {
20
- if (line && line.length + 1 + w.length > width) { lines.push(line); line = w; }
21
- else line = line ? `${line} ${w}` : w;
24
+ if (cur && cur.length + 1 + w.length > width) { lines.push(cur); cur = w; }
25
+ else cur = cur ? `${cur} ${w}` : w;
22
26
  }
23
- if (line) lines.push(line);
27
+ if (cur) lines.push(cur);
24
28
  return lines;
25
29
  }
26
30
 
27
- function writeLine(s = '') {
28
- clr();
29
- process.stdout.write(s + '\n');
30
- }
31
-
32
31
  export async function skillPicker({ message, options }) {
33
- const termW = process.stdout.columns || 100;
34
- const termH = process.stdout.rows || 30;
35
-
36
- const DESC_LINES = 3;
37
- const FOOTER_LINES = 2;
38
- const HEADER_LINES = 2;
39
- const VISIBLE = Math.min(options.length, Math.max(6, termH - HEADER_LINES - 1 - DESC_LINES - FOOTER_LINES));
40
-
41
- let cursor = 0;
42
- let scrollTop = 0;
43
- const sel = new Set();
44
-
45
- function ensureVisible() {
46
- if (cursor < scrollTop) scrollTop = cursor;
47
- if (cursor >= scrollTop + VISIBLE) scrollTop = cursor - VISIBLE + 1;
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));
48
55
  }
49
56
 
50
- function render(first = false) {
51
- const totalLines = HEADER_LINES + VISIBLE + 1 + DESC_LINES + FOOTER_LINES;
52
- if (!first) { up(totalLines); }
57
+ function renderGrid() {
58
+ const lines = [];
53
59
 
54
60
  // Header
55
- writeLine(brand('◆') + ' ' + white(message));
56
- writeLine(muted(''));
57
-
58
- // Skill list
59
- for (let i = 0; i < VISIBLE; i++) {
60
- const idx = scrollTop + i;
61
- if (idx >= options.length) { writeLine(muted('│')); continue; }
62
- const opt = options[idx];
63
- const focused = idx === cursor;
64
- const checked = sel.has(idx);
65
- const bullet = checked ? success('■') : muted('□');
66
- const nameStr = focused
67
- ? ansis.bold(skillColor(idx)(opt.label))
68
- : skillColor(idx)(opt.label);
69
- const cursor_ = focused ? brand('▶') : ' ';
70
- writeLine(`${muted('│')} ${cursor_} ${bullet} ${nameStr}`);
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);
71
86
  }
72
87
 
73
- // Separator + description panel
74
- const divW = Math.min(termW - 2, 72);
75
- writeLine(muted('├' + '─'.repeat(divW) + '┤'));
88
+ // Separator + description
89
+ lines.push(muted('├' + '─'.repeat(Math.min(termW - 2, 78)) + '┤'));
76
90
 
77
91
  const focused = options[cursor];
78
- const desc = focused?.description || '';
79
- const descW = termW - 6;
80
- const wrapped = desc ? wordWrap(desc, descW) : [];
81
-
82
- for (let i = 0; i < DESC_LINES; i++) {
92
+ const desc = focused?.description ?? '';
93
+ const wrapped = desc ? wordWrap(desc, termW - 6) : [];
94
+ for (let i = 0; i < DESC_H; i++) {
83
95
  const line = wrapped[i] ?? '';
84
- writeLine(muted('│ ') + white(line));
96
+ lines.push(muted('│ ') + white(line));
85
97
  }
86
98
 
87
99
  // Footer
88
- const scrollHint = options.length > VISIBLE
89
- ? muted(` ${scrollTop + 1}–${Math.min(scrollTop + VISIBLE, options.length)} of ${options.length}`)
90
- : '';
91
- writeLine(muted('│'));
92
- writeLine(
93
- muted(' ↑↓ navigate') + ' ' +
94
- muted('space toggle') + ' ' +
95
- muted('a = all') + ' ' +
96
- success(`${sel.size} selected`) + ' ' +
97
- muted('enter confirm') + ' ' +
98
- muted('esc back') +
99
- scrollHint
100
+ const selCount = sel.size;
101
+ lines.push(
102
+ ' ' + (selCount > 0 ? success(`${selCount} selected`) : muted('0 selected')) +
103
+ muted(` · ${cursor + 1} of ${options.length}`)
100
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;
101
119
  }
102
120
 
103
- // Reserve space
104
- process.stdout.write('\n'.repeat(HEADER_LINES + VISIBLE + 1 + DESC_LINES + FOOTER_LINES));
105
- render(false);
121
+ // Initial draw
122
+ draw();
106
123
 
107
124
  return new Promise((resolve, reject) => {
108
125
  process.stdin.setRawMode(true);
109
126
  process.stdin.resume();
110
127
  process.stdin.setEncoding('utf8');
111
128
 
112
- function done(result) {
129
+ function cleanup() {
113
130
  process.stdin.setRawMode(false);
114
131
  process.stdin.pause();
115
132
  process.stdin.removeAllListeners('data');
116
- if (result !== null) {
117
- nl();
118
- process.stdout.write(success('◆') + ' ' + white(`${result.length} skill(s) selected`) + '\n');
119
- }
120
- resolve(result);
133
+ }
134
+
135
+ function cancel() {
136
+ cleanup();
137
+ reject(Object.assign(new Error('cancel'), { isCancel: true }));
121
138
  }
122
139
 
123
140
  process.stdin.on('data', key => {
124
- if (key === CTRL_C) { done(null); reject(Object.assign(new Error('cancel'), { isCancel: true })); return; }
125
- if (key === ESC) { done(null); reject(Object.assign(new Error('cancel'), { isCancel: true })); return; }
126
-
127
- if (key === UP) {
128
- if (cursor > 0) { cursor--; ensureVisible(); }
129
- } else if (key === DOWN) {
130
- if (cursor < options.length - 1) { cursor++; ensureVisible(); }
131
- } else if (key === SPACE) {
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) {
132
153
  sel.has(cursor) ? sel.delete(cursor) : sel.add(cursor);
133
- } else if (key === 'a' || key === 'A') {
154
+ }
155
+ if (key === 'a' || key === 'A') {
134
156
  sel.size === options.length
135
157
  ? sel.clear()
136
158
  : options.forEach((_, i) => sel.add(i));
137
- } else if (key === ENTER) {
159
+ }
160
+ if (key === KEY.ENTER) {
138
161
  if (!sel.size) return;
139
- done([...sel].sort((a, b) => a - b).map(i => options[i].value));
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));
140
166
  return;
141
167
  }
142
168
 
143
- render(false);
169
+ draw();
144
170
  });
145
171
  });
146
172
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-agent-skills",
3
- "version": "1.3.5",
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.5",
4
+ "version": "1.3.6",
5
5
  "skills": [
6
6
  "ask-matt",
7
7
  "brainstorming",