skills 1.5.18 → 1.5.20

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 CHANGED
@@ -3,7 +3,7 @@
3
3
  The CLI for the open agent skills ecosystem.
4
4
 
5
5
  <!-- agent-list:start -->
6
- Supports **OpenCode**, **Claude Code**, **Codex**, **Cursor**, and [69 more](#supported-agents).
6
+ Supports **OpenCode**, **Claude Code**, **Codex**, **Cursor**, and [70 more](#supported-agents).
7
7
  <!-- agent-list:end -->
8
8
 
9
9
  [![skills.sh](https://skills.sh/b/vercel-labs/skills)](https://skills.sh/vercel-labs/skills)
@@ -272,12 +272,14 @@ Skills can be installed to any of these agents:
272
272
  | Gemini CLI | `gemini-cli` | `.agents/skills/` | `~/.gemini/skills/` |
273
273
  | GitHub Copilot | `github-copilot` | `.agents/skills/` | `~/.copilot/skills/` |
274
274
  | Goose | `goose` | `.goose/skills/` | `~/.config/goose/skills/` |
275
+ | Grok Build | `grok` | `.grok/skills/` | `~/.grok/skills/` |
275
276
  | Hermes Agent | `hermes-agent` | `.hermes/skills/` | `~/.hermes/skills/` |
276
277
  | inference.sh | `inference-sh` | `.inferencesh/skills/` | `~/.inferencesh/skills/` |
277
278
  | Jazz | `jazz` | `.jazz/skills/` | `~/.jazz/skills/` |
278
279
  | Junie | `junie` | `.junie/skills/` | `~/.junie/skills/` |
279
280
  | iFlow CLI | `iflow-cli` | `.iflow/skills/` | `~/.iflow/skills/` |
280
281
  | Kilo Code | `kilo` | `.kilocode/skills/` | `~/.kilocode/skills/` |
282
+ | Kimchi | `kimchi` | `.kimchi/skills/` | `~/.config/kimchi/harness/skills/` |
281
283
  | Kiro CLI | `kiro-cli` | `.kiro/skills/` | `~/.kiro/skills/` |
282
284
  | Kode | `kode` | `.kode/skills/` | `~/.kode/skills/` |
283
285
  | Lingma | `lingma` | `.lingma/skills/` | `~/.lingma/skills/` |
@@ -402,12 +404,14 @@ to also discover `SKILL.md` files outside these container directories
402
404
  - `agent/skills/`
403
405
  - `.forge/skills/`
404
406
  - `.goose/skills/`
407
+ - `.grok/skills/`
405
408
  - `.hermes/skills/`
406
409
  - `.inferencesh/skills/`
407
410
  - `.jazz/skills/`
408
411
  - `.junie/skills/`
409
412
  - `.iflow/skills/`
410
413
  - `.kilocode/skills/`
414
+ - `.kimchi/skills/`
411
415
  - `.kiro/skills/`
412
416
  - `.kode/skills/`
413
417
  - `.lingma/skills/`
@@ -627,60 +627,6 @@ var r = class extends V {
627
627
  });
628
628
  }
629
629
  };
630
- let u$2 = class u extends V {
631
- options;
632
- cursor = 0;
633
- #t;
634
- getGroupItems(t) {
635
- return this.options.filter((r) => r.group === t);
636
- }
637
- isGroupSelected(t) {
638
- const r = this.getGroupItems(t), e = this.value;
639
- return e === void 0 ? false : r.every((s) => e.includes(s.value));
640
- }
641
- toggleValue() {
642
- const t = this.options[this.cursor];
643
- if (t !== void 0) if (this.value === void 0 && (this.value = []), t.group === true) {
644
- const r = t.value, e = this.getGroupItems(r);
645
- this.isGroupSelected(r) ? this.value = this.value.filter((s) => e.findIndex((i) => i.value === s) === -1) : this.value = [...this.value, ...e.map((s) => s.value)], this.value = Array.from(new Set(this.value));
646
- } else {
647
- const r = this.value.includes(t.value);
648
- this.value = r ? this.value.filter((e) => e !== t.value) : [...this.value, t.value];
649
- }
650
- }
651
- constructor(t) {
652
- super(t, false);
653
- const { options: r } = t;
654
- this.#t = t.selectableGroups !== false, this.options = Object.entries(r).flatMap(([e, s]) => [{
655
- value: e,
656
- group: true,
657
- label: e
658
- }, ...s.map((i) => ({
659
- ...i,
660
- group: e
661
- }))]), this.value = [...t.initialValues ?? []], this.cursor = Math.max(this.options.findIndex(({ value: e }) => e === t.cursorAt), this.#t ? 0 : 1), this.on("cursor", (e) => {
662
- switch (e) {
663
- case "left":
664
- case "up": {
665
- this.cursor = this.cursor === 0 ? this.options.length - 1 : this.cursor - 1;
666
- const s = this.options[this.cursor]?.group === true;
667
- !this.#t && s && (this.cursor = this.cursor === 0 ? this.options.length - 1 : this.cursor - 1);
668
- break;
669
- }
670
- case "down":
671
- case "right": {
672
- this.cursor = this.cursor === this.options.length - 1 ? 0 : this.cursor + 1;
673
- const s = this.options[this.cursor]?.group === true;
674
- !this.#t && s && (this.cursor = this.cursor === this.options.length - 1 ? 0 : this.cursor + 1);
675
- break;
676
- }
677
- case "space":
678
- this.toggleValue();
679
- break;
680
- }
681
- });
682
- }
683
- };
684
630
  var a = class extends V {
685
631
  options;
686
632
  cursor = 0;
@@ -755,4 +701,4 @@ let n$1 = class n extends V {
755
701
  });
756
702
  }
757
703
  };
758
- export { a, block, fastStringWidth, getColumns, getRows, isCancel, n$1, r, require_src, settings, u$2, wrapAnsi, wrapTextWithPrefix };
704
+ export { a, block, fastStringWidth, getColumns, getRows, isCancel, n$1, r, require_src, settings, wrapAnsi, wrapTextWithPrefix };
@@ -1,4 +1,4 @@
1
- import { a, block, fastStringWidth, getColumns, getRows, n$1, r, require_src, settings, u$2, wrapAnsi, wrapTextWithPrefix } from "./core.mjs";
1
+ import { a, block, fastStringWidth, getColumns, getRows, n$1, r, require_src, settings, wrapAnsi, wrapTextWithPrefix } from "./core.mjs";
2
2
  import { styleText } from "node:util";
3
3
  import process$1 from "node:process";
4
4
  var import_src = require_src();
@@ -213,100 +213,6 @@ ${h}
213
213
  }
214
214
  }).prompt();
215
215
  };
216
- const groupMultiselect = (o) => {
217
- const { selectableGroups: h = true, groupSpacing: x = 0 } = o, m = (n, l, g = []) => {
218
- const a = n.label ?? String(n.value), t = typeof n.group == "string", s = t && (g[g.indexOf(n) + 1] ?? { group: true }), u = t && s && s.group === true;
219
- let r = "", c = "";
220
- t && (h ? (r = u ? `${S_BAR_END} ` : `${S_BAR} `, c = u ? " " : `${S_BAR} `) : r = " ");
221
- let i = "";
222
- if (x > 0 && !t && (i = `
223
- `.repeat(x)), l === "active") return wrapTextWithPrefix(o.output, `${a}${n.hint ? ` ${styleText("dim", `(${n.hint})`)}` : ""}`, `${i}${styleText("dim", r)} `, `${i}${styleText("dim", r)}${styleText("cyan", S_CHECKBOX_ACTIVE)} `, `${i}${styleText("dim", c)} `);
224
- if (l === "group-active") return wrapTextWithPrefix(o.output, a, `${i}${r} `, `${i}${r}${styleText("cyan", S_CHECKBOX_ACTIVE)} `, `${i}${c} `, (d) => styleText("dim", d));
225
- if (l === "group-active-selected") return wrapTextWithPrefix(o.output, a, `${i}${r} `, `${i}${r}${styleText("green", S_CHECKBOX_SELECTED)} `, `${i}${c} `, (d) => styleText("dim", d));
226
- if (l === "selected") {
227
- const d = t || h ? styleText("green", S_CHECKBOX_SELECTED) : "";
228
- return wrapTextWithPrefix(o.output, `${a}${n.hint ? ` (${n.hint})` : ""}`, `${i}${styleText("dim", r)} `, `${i}${styleText("dim", r)}${d} `, `${i}${styleText("dim", c)} `, (S) => styleText("dim", S));
229
- }
230
- if (l === "cancelled") return `${styleText(["strikethrough", "dim"], a)}`;
231
- if (l === "active-selected") return wrapTextWithPrefix(o.output, `${a}${n.hint ? ` ${styleText("dim", `(${n.hint})`)}` : ""}`, `${i}${styleText("dim", r)} `, `${i}${styleText("dim", r)}${styleText("green", S_CHECKBOX_SELECTED)} `, `${i}${styleText("dim", c)} `);
232
- if (l === "submitted") return `${styleText("dim", a)}`;
233
- const f = t || h ? styleText("dim", S_CHECKBOX_INACTIVE) : "";
234
- return wrapTextWithPrefix(o.output, a, `${i}${styleText("dim", r)} `, `${i}${styleText("dim", r)}${f} `, `${i}${styleText("dim", c)} `, (d) => styleText("dim", d));
235
- }, y = o.required ?? true, I = o.showInstructions ?? true;
236
- return new u$2({
237
- options: o.options,
238
- signal: o.signal,
239
- input: o.input,
240
- output: o.output,
241
- initialValues: o.initialValues,
242
- required: y,
243
- cursorAt: o.cursorAt,
244
- selectableGroups: h,
245
- validate(n) {
246
- if (y && (n === void 0 || n.length === 0)) return `Please select at least one option.
247
- ${styleText("reset", styleText("dim", `Press ${styleText([
248
- "gray",
249
- "bgWhite",
250
- "inverse"
251
- ], " space ")} to select, ${styleText("gray", styleText(["bgWhite", "inverse"], " enter "))} to submit`))}`;
252
- },
253
- render() {
254
- const n = o.withGuide ?? settings.withGuide, l = `${n ? `${styleText("gray", S_BAR)}
255
- ` : ""}${symbol(this.state)} ${o.message}
256
- `, g = this.value ?? [], a = (t, s) => {
257
- const u = this.options, r = g.includes(t.value) || t.group === true && this.isGroupSelected(`${t.value}`);
258
- return !s && typeof t.group == "string" && this.options[this.cursor]?.value === t.group ? m(t, r ? "group-active-selected" : "group-active", u) : s && r ? m(t, "active-selected", u) : r ? m(t, "selected", u) : m(t, s ? "active" : "inactive", u);
259
- };
260
- switch (this.state) {
261
- case "submit": {
262
- const t = this.options.filter(({ value: u }) => g.includes(u)).map((u) => m(u, "submitted")), s = t.length === 0 ? "" : ` ${t.join(styleText("dim", ", "))}`;
263
- return `${l}${n ? styleText("gray", S_BAR) : ""}${s}`;
264
- }
265
- case "cancel": {
266
- const t = this.options.filter(({ value: s }) => g.includes(s)).map((s) => m(s, "cancelled")).join(styleText("dim", ", "));
267
- return `${l}${n ? `${styleText("gray", S_BAR)} ` : ""}${t.trim() ? `${t}${n ? `
268
- ${styleText("gray", S_BAR)}` : ""}` : ""}`;
269
- }
270
- case "error": {
271
- const t = n ? `${styleText("yellow", S_BAR)} ` : "", s = this.error.split(`
272
- `).map((i, f) => f === 0 ? `${n ? `${styleText("yellow", S_BAR_END)} ` : ""}${styleText("yellow", i)}` : ` ${i}`).join(`
273
- `), u = l.split(`
274
- `).length, r = s.split(`
275
- `).length + 1;
276
- return `${l}${t}${limitOptions({
277
- output: o.output,
278
- options: this.options,
279
- cursor: this.cursor,
280
- maxItems: o.maxItems,
281
- columnPadding: t.length,
282
- rowPadding: u + r,
283
- style: a
284
- }).join(`
285
- ${t}`)}
286
- ${s}
287
- `;
288
- }
289
- default: {
290
- const t = n ? `${styleText("cyan", S_BAR)} ` : "", s = l.split(`
291
- `).length, u = I ? formatInstructionFooter(MULTISELECT_INSTRUCTIONS, n) : n ? [styleText("cyan", S_BAR_END)] : [], r = u.join(`
292
- `), c = u.length + 1;
293
- return `${l}${t}${limitOptions({
294
- output: o.output,
295
- options: this.options,
296
- cursor: this.cursor,
297
- maxItems: o.maxItems,
298
- columnPadding: t.length,
299
- rowPadding: s + c,
300
- style: a
301
- }).join(`
302
- ${t}`)}
303
- ${r}
304
- `;
305
- }
306
- }
307
- }
308
- }).prompt();
309
- };
310
216
  const log = {
311
217
  message: (s = [], { symbol: e = styleText("gray", S_BAR), secondarySymbol: r = styleText("gray", S_BAR), output: m = process.stdout, spacing: l = 1, withGuide: c } = {}) => {
312
218
  const t = [], o = c ?? settings.withGuide, f = o ? r : "", O = o ? `${e} ` : "", u = o ? `${r} ` : "";
@@ -536,4 +442,4 @@ ${b}
536
442
  }).prompt();
537
443
  };
538
444
  `${styleText("gray", S_BAR)}`;
539
- export { cancel, confirm, groupMultiselect, intro, log, multiselect, note, outro, select, spinner };
445
+ export { cancel, confirm, intro, log, multiselect, note, outro, select, spinner };
package/dist/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { __toESM } from "./_chunks/rolldown-runtime.mjs";
3
3
  import { isCancel } from "./_chunks/libs/@clack/core.mjs";
4
- import { cancel, confirm, groupMultiselect, intro, log, multiselect as multiselect$1, note, outro, select, spinner } from "./_chunks/libs/@clack/prompts.mjs";
4
+ import { cancel, confirm, intro, log, multiselect as multiselect$1, note, outro, select, spinner } from "./_chunks/libs/@clack/prompts.mjs";
5
5
  import { require_picocolors } from "./_chunks/libs/picocolors.mjs";
6
6
  import { esm_default } from "./_chunks/libs/simple-git.mjs";
7
7
  import { xdgConfig } from "./_chunks/libs/xdg-basedir.mjs";
@@ -21,6 +21,22 @@ import { createHash } from "crypto";
21
21
  import { createHash as createHash$1 } from "node:crypto";
22
22
  import { gunzipSync, inflateRawSync } from "node:zlib";
23
23
  var import_picocolors = /* @__PURE__ */ __toESM(require_picocolors(), 1);
24
+ const DEFAULT_GITHUB_HOST = "github.com";
25
+ function getGitHubHost() {
26
+ const configuredHost = process.env.GH_HOST?.trim();
27
+ if (!configuredHost) return DEFAULT_GITHUB_HOST;
28
+ try {
29
+ const parsed = new URL(`https://${configuredHost}`);
30
+ if (parsed.username || parsed.password || parsed.port || parsed.pathname !== "/" || parsed.search || parsed.hash) return DEFAULT_GITHUB_HOST;
31
+ return parsed.hostname;
32
+ } catch {
33
+ return DEFAULT_GITHUB_HOST;
34
+ }
35
+ }
36
+ function isGitHubHost(host) {
37
+ const normalizedHost = host.toLowerCase();
38
+ return normalizedHost === DEFAULT_GITHUB_HOST || normalizedHost === getGitHubHost().toLowerCase();
39
+ }
24
40
  function getOwnerRepo(parsed) {
25
41
  if (parsed.type === "local") return null;
26
42
  const sshMatch = parsed.url.match(/^git@[^:]+:(.+)$/);
@@ -71,7 +87,10 @@ function sanitizeSubpath(subpath) {
71
87
  function isLocalPath(input) {
72
88
  return isAbsolute(input) || input.startsWith("./") || input.startsWith("../") || input === "." || input === ".." || /^[a-zA-Z]:[/\\]/.test(input);
73
89
  }
74
- const SOURCE_ALIASES = { "coinbase/agentWallet": "coinbase/agentic-wallet-skills" };
90
+ const SOURCE_ALIASES = {
91
+ "coinbase/agentWallet": "coinbase/agentic-wallet-skills",
92
+ "vercel-labs/vercel-skills": "vercel-labs/agent-skills"
93
+ };
75
94
  function decodeFragmentValue(value) {
76
95
  try {
77
96
  return decodeURIComponent(value);
@@ -85,7 +104,7 @@ function looksLikeGitSource(input) {
85
104
  if (input.startsWith("http://") || input.startsWith("https://")) try {
86
105
  const parsed = new URL(input);
87
106
  const pathname = parsed.pathname;
88
- if (parsed.hostname === "github.com") return /^\/[^/]+\/[^/]+(?:\.git)?(?:\/tree\/[^/]+(?:\/.*)?)?\/?$/.test(pathname);
107
+ if (isGitHubHost(parsed.host)) return /^\/[^/]+\/[^/]+(?:\.git)?(?:\/tree\/[^/]+(?:\/.*)?)?\/?$/.test(pathname);
89
108
  if (parsed.hostname === "gitlab.com") return /^\/.+?\/[^/]+(?:\.git)?(?:\/-\/tree\/[^/]+(?:\/.*)?)?\/?$/.test(pathname);
90
109
  } catch {}
91
110
  if (/^https?:\/\/.+\.git(?:$|[/?])/i.test(input)) return true;
@@ -131,6 +150,22 @@ function parseSource(input) {
131
150
  if (githubPrefixMatch) return parseSource(appendFragmentRef(githubPrefixMatch[1], fragmentRef, fragmentSkillFilter));
132
151
  const gitlabPrefixMatch = input.match(/^gitlab:(.+)$/);
133
152
  if (gitlabPrefixMatch) return parseSource(appendFragmentRef(`https://gitlab.com/${gitlabPrefixMatch[1]}`, fragmentRef, fragmentSkillFilter));
153
+ if (getGitHubHost() !== "github.com" && /^https?:\/\//.test(input)) try {
154
+ const parsedUrl = new URL(input);
155
+ if (isGitHubHost(parsedUrl.host) && parsedUrl.host !== "github.com") {
156
+ const [owner, rawRepo, marker, ref, ...subpathSegments] = parsedUrl.pathname.split("/").filter(Boolean);
157
+ if (owner && rawRepo) {
158
+ const repo = rawRepo.replace(/\.git$/, "");
159
+ const isTreeUrl = marker === "tree" && ref;
160
+ return {
161
+ type: "git",
162
+ url: `${parsedUrl.protocol}//${parsedUrl.host}/${owner}/${repo}.git`,
163
+ ...isTreeUrl ? { ref } : fragmentRef ? { ref: fragmentRef } : {},
164
+ ...isTreeUrl && subpathSegments.length > 0 ? { subpath: sanitizeSubpath(subpathSegments.join("/")) } : {}
165
+ };
166
+ }
167
+ }
168
+ } catch {}
134
169
  const githubTreeWithPathMatch = input.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+)/);
135
170
  if (githubTreeWithPathMatch) {
136
171
  const [, owner, repo, ref, subpath] = githubTreeWithPathMatch;
@@ -187,12 +222,14 @@ function parseSource(input) {
187
222
  ...fragmentRef ? { ref: fragmentRef } : {}
188
223
  };
189
224
  }
225
+ const githubHost = getGitHubHost();
226
+ const shorthandSourceType = githubHost === "github.com" ? "github" : "git";
190
227
  const atSkillMatch = input.match(/^([^/]+)\/([^/@]+)@(.+)$/);
191
228
  if (atSkillMatch && !input.includes(":") && !input.startsWith(".") && !input.startsWith("/")) {
192
229
  const [, owner, repo, skillFilter] = atSkillMatch;
193
230
  return {
194
- type: "github",
195
- url: `https://github.com/${owner}/${repo}.git`,
231
+ type: shorthandSourceType,
232
+ url: `https://${githubHost}/${owner}/${repo}.git`,
196
233
  ...fragmentRef ? { ref: fragmentRef } : {},
197
234
  skillFilter: fragmentSkillFilter || skillFilter
198
235
  };
@@ -201,8 +238,8 @@ function parseSource(input) {
201
238
  if (shorthandMatch && !input.includes(":") && !input.startsWith(".") && !input.startsWith("/")) {
202
239
  const [, owner, repo, subpath] = shorthandMatch;
203
240
  return {
204
- type: "github",
205
- url: `https://github.com/${owner}/${repo}.git`,
241
+ type: shorthandSourceType,
242
+ url: `https://${githubHost}/${owner}/${repo}.git`,
206
243
  ...fragmentRef ? { ref: fragmentRef } : {},
207
244
  subpath: subpath ? sanitizeSubpath(subpath) : subpath,
208
245
  ...fragmentSkillFilter ? { skillFilter: fragmentSkillFilter } : {}
@@ -277,8 +314,88 @@ function countVisualRowsForLines(lines, columns) {
277
314
  const cols = columns !== void 0 && columns > 0 ? columns : process.stdout.columns && process.stdout.columns > 0 ? process.stdout.columns : 80;
278
315
  return lines.reduce((sum, line) => sum + visualRowsForLine(line, cols), 0);
279
316
  }
317
+ function truncateToWidth(text, width) {
318
+ let truncated = "";
319
+ for (const char of text) {
320
+ if (approxStringWidth(truncated + char) > width) break;
321
+ truncated += char;
322
+ }
323
+ return truncated;
324
+ }
325
+ function formatDetailLines(detail, width, maxLines) {
326
+ const safeWidth = Math.max(1, width);
327
+ const normalized = detail?.replace(/\s+/g, " ").trim() ?? "";
328
+ const lines = [];
329
+ let remaining = normalized;
330
+ while (remaining && lines.length < maxLines) {
331
+ if (approxStringWidth(remaining) <= safeWidth) {
332
+ lines.push(remaining);
333
+ remaining = "";
334
+ break;
335
+ }
336
+ const candidate = truncateToWidth(remaining, safeWidth);
337
+ const breakAt = candidate.lastIndexOf(" ");
338
+ if (breakAt > 0) {
339
+ lines.push(candidate.slice(0, breakAt).trimEnd());
340
+ remaining = remaining.slice(breakAt).trimStart();
341
+ } else {
342
+ lines.push(candidate);
343
+ remaining = remaining.slice(candidate.length).trimStart();
344
+ }
345
+ }
346
+ if (remaining && lines.length > 0) {
347
+ const last = lines.length - 1;
348
+ lines[last] = `${truncateToWidth(lines[last], Math.max(0, safeWidth - 1)).trimEnd()}…`;
349
+ }
350
+ while (lines.length < maxLines) lines.push("");
351
+ return lines;
352
+ }
353
+ function buildSearchEntries(items, selectGroups, collapsedGroups = /* @__PURE__ */ new Set()) {
354
+ if (!selectGroups) return items.map((item) => ({
355
+ type: "item",
356
+ item
357
+ }));
358
+ const entries = [];
359
+ let index = 0;
360
+ while (index < items.length) {
361
+ const item = items[index];
362
+ if (!item.group) {
363
+ entries.push({
364
+ type: "item",
365
+ item
366
+ });
367
+ index += 1;
368
+ continue;
369
+ }
370
+ const groupItems = [];
371
+ while (index < items.length && items[index].group === item.group) {
372
+ groupItems.push(items[index]);
373
+ index += 1;
374
+ }
375
+ const collapsed = collapsedGroups.has(item.group);
376
+ entries.push({
377
+ type: "group",
378
+ group: item.group,
379
+ items: groupItems,
380
+ collapsed
381
+ });
382
+ if (!collapsed) entries.push(...groupItems.map((groupItem) => ({
383
+ type: "item",
384
+ item: groupItem
385
+ })));
386
+ }
387
+ return entries;
388
+ }
389
+ function toggleSearchEntry(selected, entry) {
390
+ if (entry?.type === "group") {
391
+ const allSelected = entry.items.every((item) => selected.has(item.value));
392
+ for (const item of entry.items) if (allSelected) selected.delete(item.value);
393
+ else selected.add(item.value);
394
+ } else if (entry?.type === "item") if (selected.has(entry.item.value)) selected.delete(entry.item.value);
395
+ else selected.add(entry.item.value);
396
+ }
280
397
  async function searchMultiselect(options) {
281
- const { message, items, maxVisible = 8, initialSelected = [], required = false, lockedSection } = options;
398
+ const { message, items, maxVisible = 8, initialSelected = [], required = false, lockedSection, searchable = true, showDetail = false, detailLines = 2, showSelectedSummary = true, selectGroups = false } = options;
282
399
  return new Promise((resolve) => {
283
400
  const rl = readline.createInterface({
284
401
  input: process.stdin,
@@ -290,6 +407,7 @@ async function searchMultiselect(options) {
290
407
  let query = "";
291
408
  let cursor = 0;
292
409
  const selected = new Set(initialSelected);
410
+ const collapsedGroups = /* @__PURE__ */ new Set();
293
411
  let lastRenderHeight = 0;
294
412
  const lockedValues = lockedSection ? lockedSection.items.map((i) => i.value) : [];
295
413
  const filter = (item, q) => {
@@ -300,17 +418,10 @@ async function searchMultiselect(options) {
300
418
  const getFiltered = () => {
301
419
  return items.filter((item) => filter(item, query));
302
420
  };
303
- const clearRender = () => {
304
- if (lastRenderHeight > 0) {
305
- process.stdout.write(`\x1b[${lastRenderHeight}A`);
306
- for (let i = 0; i < lastRenderHeight; i++) process.stdout.write("\x1B[2K\x1B[1B");
307
- process.stdout.write(`\x1b[${lastRenderHeight}A`);
308
- }
309
- };
310
421
  const render = (state = "active") => {
311
- clearRender();
312
422
  const lines = [];
313
423
  const filtered = getFiltered();
424
+ const entries = buildSearchEntries(filtered, selectGroups, collapsedGroups);
314
425
  const icon = state === "active" ? S_STEP_ACTIVE : state === "cancel" ? S_STEP_CANCEL : S_STEP_SUBMIT;
315
426
  lines.push(`${icon} ${import_picocolors.default.bold(message)}`);
316
427
  if (state === "active") {
@@ -323,28 +434,41 @@ async function searchMultiselect(options) {
323
434
  lines.push(`${S_BAR}`);
324
435
  lines.push(`${S_BAR} ${S_BAR_H}${S_BAR_H} ${import_picocolors.default.bold("Additional agents")} ${S_BAR_H.repeat(29)}`);
325
436
  }
326
- const searchLine = `${S_BAR} ${import_picocolors.default.dim("Search:")} ${query}${import_picocolors.default.inverse(" ")}`;
327
- lines.push(searchLine);
328
- lines.push(`${S_BAR} ${import_picocolors.default.dim("↑↓ move, space select, enter confirm")}`);
329
- lines.push(`${S_BAR}`);
330
- const visibleStart = Math.max(0, Math.min(cursor - Math.floor(maxVisible / 2), filtered.length - maxVisible));
331
- const visibleEnd = Math.min(filtered.length, visibleStart + maxVisible);
332
- const visibleItems = filtered.slice(visibleStart, visibleEnd);
437
+ if (searchable) {
438
+ const searchLine = `${S_BAR} ${import_picocolors.default.dim("Search:")} ${query}${import_picocolors.default.inverse(" ")}`;
439
+ lines.push(searchLine);
440
+ lines.push(`${S_BAR} ${import_picocolors.default.dim("↑↓ move, space select, enter confirm")}`);
441
+ lines.push(`${S_BAR}`);
442
+ }
443
+ const visibleStart = Math.max(0, Math.min(cursor - Math.floor(maxVisible / 2), entries.length - maxVisible));
444
+ const visibleEnd = Math.min(entries.length, visibleStart + maxVisible);
445
+ const visibleEntries = entries.slice(visibleStart, visibleEnd);
333
446
  if (filtered.length === 0) lines.push(`${S_BAR} ${import_picocolors.default.dim("No matches found")}`);
334
447
  else {
335
- for (let i = 0; i < visibleItems.length; i++) {
336
- const item = visibleItems[i];
337
- const actualIndex = visibleStart + i;
338
- const isSelected = selected.has(item.value);
339
- const isCursor = actualIndex === cursor;
340
- const radio = isSelected ? S_RADIO_ACTIVE : S_RADIO_INACTIVE;
448
+ for (let i = 0; i < visibleEntries.length; i++) {
449
+ const entry = visibleEntries[i];
450
+ const isCursor = visibleStart + i === cursor;
451
+ if (entry.type === "group") {
452
+ const selectedCount = entry.items.filter((item) => selected.has(item.value)).length;
453
+ const radio = selectedCount === entry.items.length ? S_RADIO_ACTIVE : selectedCount > 0 ? import_picocolors.default.yellow("◐") : S_RADIO_INACTIVE;
454
+ const label = isCursor ? import_picocolors.default.underline(import_picocolors.default.bold(entry.group)) : import_picocolors.default.bold(entry.group);
455
+ const prefix = isCursor ? import_picocolors.default.cyan("❯") : " ";
456
+ const disclosure = import_picocolors.default.dim(entry.collapsed ? "▸" : "▾");
457
+ lines.push(`${S_BAR} ${prefix} ${disclosure} ${radio} ${label}`);
458
+ continue;
459
+ }
460
+ const item = entry.item;
461
+ const radio = selected.has(item.value) ? S_RADIO_ACTIVE : S_RADIO_INACTIVE;
341
462
  const label = isCursor ? import_picocolors.default.underline(item.label) : item.label;
342
463
  const hint = item.hint ? import_picocolors.default.dim(` (${item.hint})`) : "";
343
464
  const prefix = isCursor ? import_picocolors.default.cyan("❯") : " ";
344
- lines.push(`${S_BAR} ${prefix} ${radio} ${label}${hint}`);
465
+ const groupItems = selectGroups && item.group ? filtered.filter((i) => i.group === item.group) : [];
466
+ const isLastInGroup = groupItems.at(-1) === item;
467
+ const tree = groupItems.length > 0 ? `${import_picocolors.default.dim(isLastInGroup ? "└─" : "├─")} ` : "";
468
+ lines.push(`${S_BAR} ${prefix} ${tree}${radio} ${label}${hint}`);
345
469
  }
346
470
  const hiddenBefore = visibleStart;
347
- const hiddenAfter = filtered.length - visibleEnd;
471
+ const hiddenAfter = entries.length - visibleEnd;
348
472
  if (hiddenBefore > 0 || hiddenAfter > 0) {
349
473
  const parts = [];
350
474
  if (hiddenBefore > 0) parts.push(`↑ ${hiddenBefore} more`);
@@ -352,19 +476,35 @@ async function searchMultiselect(options) {
352
476
  lines.push(`${S_BAR} ${import_picocolors.default.dim(parts.join(" "))}`);
353
477
  }
354
478
  }
355
- lines.push(`${S_BAR}`);
356
- const allSelectedLabels = [...lockedSection ? lockedSection.items.map((i) => i.label) : [], ...items.filter((item) => selected.has(item.value)).map((item) => item.label)];
357
- if (allSelectedLabels.length === 0) lines.push(`${S_BAR} ${import_picocolors.default.dim("Selected: (none)")}`);
358
- else {
359
- const summary = allSelectedLabels.length <= 3 ? allSelectedLabels.join(", ") : `${allSelectedLabels.slice(0, 3).join(", ")} +${allSelectedLabels.length - 3} more`;
360
- lines.push(`${S_BAR} ${import_picocolors.default.green("Selected:")} ${summary}`);
479
+ if (showDetail) {
480
+ const entry = entries[cursor];
481
+ const detail = entry?.type === "group" ? `Select all ${entry.items.length} skills in ${entry.group}.` : entry?.item.detail;
482
+ const columns = process.stdout.columns && process.stdout.columns > 0 ? process.stdout.columns : 80;
483
+ const detailWidth = Math.max(1, columns - 5);
484
+ lines.push(`${S_BAR}`);
485
+ lines.push(`${S_BAR} ${import_picocolors.default.dim("Description")}`);
486
+ for (const line of formatDetailLines(detail, detailWidth, detailLines)) lines.push(`${S_BAR} ${import_picocolors.default.dim(line)}`);
487
+ }
488
+ if (showSelectedSummary) {
489
+ lines.push(`${S_BAR}`);
490
+ const allSelectedLabels = [...lockedSection ? lockedSection.items.map((i) => i.label) : [], ...items.filter((item) => selected.has(item.value)).map((item) => item.label)];
491
+ if (allSelectedLabels.length === 0) lines.push(`${S_BAR} ${import_picocolors.default.dim("Selected: (none)")}`);
492
+ else {
493
+ const summary = allSelectedLabels.length <= 3 ? allSelectedLabels.join(", ") : `${allSelectedLabels.slice(0, 3).join(", ")} +${allSelectedLabels.length - 3} more`;
494
+ lines.push(`${S_BAR} ${import_picocolors.default.green("Selected:")} ${summary}`);
495
+ }
496
+ }
497
+ if (!searchable) {
498
+ lines.push(`${S_BAR}`);
499
+ lines.push(`${S_BAR} ${import_picocolors.default.dim("↑↓ move, ←→ collapse/expand, space select, enter confirm")}`);
361
500
  }
362
501
  lines.push(`${import_picocolors.default.dim("└")}`);
363
502
  } else if (state === "submit") {
364
503
  const allSelectedLabels = [...lockedSection ? lockedSection.items.map((i) => i.label) : [], ...items.filter((item) => selected.has(item.value)).map((item) => item.label)];
365
504
  lines.push(`${S_BAR} ${import_picocolors.default.dim(allSelectedLabels.join(", "))}`);
366
505
  } else if (state === "cancel") lines.push(`${S_BAR} ${import_picocolors.default.strikethrough(import_picocolors.default.dim("Cancelled"))}`);
367
- process.stdout.write(lines.join("\n") + "\n");
506
+ const clearPreviousFrame = lastRenderHeight > 0 ? `\x1b[${lastRenderHeight}A\x1b[J` : "";
507
+ process.stdout.write(clearPreviousFrame + lines.join("\n") + "\n");
368
508
  lastRenderHeight = countVisualRowsForLines(lines, process.stdout.columns);
369
509
  };
370
510
  const cleanup = () => {
@@ -385,7 +525,7 @@ async function searchMultiselect(options) {
385
525
  };
386
526
  const keypressHandler = (_str, key) => {
387
527
  if (!key) return;
388
- const filtered = getFiltered();
528
+ const entries = buildSearchEntries(getFiltered(), selectGroups, collapsedGroups);
389
529
  if (key.name === "return") {
390
530
  submit();
391
531
  return;
@@ -400,14 +540,31 @@ async function searchMultiselect(options) {
400
540
  return;
401
541
  }
402
542
  if (key.name === "down") {
403
- cursor = Math.min(filtered.length - 1, cursor + 1);
543
+ cursor = Math.min(entries.length - 1, cursor + 1);
404
544
  render();
405
545
  return;
406
546
  }
547
+ if (selectGroups && key.name === "right") {
548
+ const entry = entries[cursor];
549
+ if (entry?.type === "group" && entry.collapsed) {
550
+ collapsedGroups.delete(entry.group);
551
+ render();
552
+ }
553
+ return;
554
+ }
555
+ if (selectGroups && key.name === "left") {
556
+ const entry = entries[cursor];
557
+ const group = entry?.type === "group" ? entry.group : entry?.item.group;
558
+ if (group) {
559
+ collapsedGroups.add(group);
560
+ cursor = buildSearchEntries(getFiltered(), selectGroups, collapsedGroups).findIndex((collapsedEntry) => collapsedEntry.type === "group" && collapsedEntry.group === group);
561
+ render();
562
+ }
563
+ return;
564
+ }
407
565
  if (key.name === "space") {
408
- const item = filtered[cursor];
409
- if (item) if (selected.has(item.value)) selected.delete(item.value);
410
- else selected.add(item.value);
566
+ const entry = entries[cursor];
567
+ toggleSearchEntry(selected, entry);
411
568
  render();
412
569
  return;
413
570
  }
@@ -417,7 +574,7 @@ async function searchMultiselect(options) {
417
574
  render();
418
575
  return;
419
576
  }
420
- if (key.sequence && !key.ctrl && !key.meta && key.sequence.length === 1) {
577
+ if (searchable && key.sequence && !key.ctrl && !key.meta && key.sequence.length === 1) {
421
578
  query += key.sequence;
422
579
  cursor = 0;
423
580
  render();
@@ -429,6 +586,7 @@ async function searchMultiselect(options) {
429
586
  });
430
587
  }
431
588
  const DEFAULT_CLONE_TIMEOUT_MS = 3e5;
589
+ const ALLOWED_GIT_PROTOCOLS = "https:http:ssh:git:file";
432
590
  const CLONE_TIMEOUT_MS = (() => {
433
591
  const raw = process.env.SKILLS_CLONE_TIMEOUT_MS;
434
592
  if (!raw) return DEFAULT_CLONE_TIMEOUT_MS;
@@ -449,20 +607,21 @@ var GitCloneError = class extends Error {
449
607
  }
450
608
  };
451
609
  function parseGitHubRepoUrl(url) {
452
- const sshMatch = url.match(/^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/i);
453
- if (sshMatch) {
454
- const owner = sshMatch[1];
455
- const repo = sshMatch[2];
610
+ const sshMatch = url.match(/^git@([^:]+):([^/]+)\/([^/]+?)(?:\.git)?$/i);
611
+ if (sshMatch && isGitHubHost(sshMatch[1])) {
612
+ const host = sshMatch[1];
613
+ const owner = sshMatch[2];
614
+ const repo = sshMatch[3];
456
615
  return {
457
616
  owner,
458
617
  repo,
459
618
  slug: `${owner}/${repo}`,
460
- sshUrl: `git@github.com:${owner}/${repo}.git`
619
+ sshUrl: `git@${host}:${owner}/${repo}.git`
461
620
  };
462
621
  }
463
622
  try {
464
623
  const parsed = new URL(url);
465
- if (parsed.hostname !== "github.com") return null;
624
+ if (!isGitHubHost(parsed.host)) return null;
466
625
  const match = parsed.pathname.match(/^\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/);
467
626
  if (!match) return null;
468
627
  const owner = match[1];
@@ -471,7 +630,7 @@ function parseGitHubRepoUrl(url) {
471
630
  owner,
472
631
  repo,
473
632
  slug: `${owner}/${repo}`,
474
- sshUrl: `git@github.com:${owner}/${repo}.git`
633
+ sshUrl: `git@${parsed.host}:${owner}/${repo}.git`
475
634
  };
476
635
  } catch {
477
636
  return null;
@@ -480,7 +639,7 @@ function parseGitHubRepoUrl(url) {
480
639
  function isGitHubHttpsCloneUrl(url) {
481
640
  try {
482
641
  const parsed = new URL(url);
483
- return parsed.protocol === "https:" && parsed.hostname === "github.com";
642
+ return parsed.protocol === "https:" && isGitHubHost(parsed.host);
484
643
  } catch {
485
644
  return false;
486
645
  }
@@ -526,6 +685,7 @@ function createGitClient(sshCommand) {
526
685
  git.env({
527
686
  ...process.env,
528
687
  GIT_TERMINAL_PROMPT: "0",
688
+ GIT_ALLOW_PROTOCOL: ALLOWED_GIT_PROTOCOLS,
529
689
  GIT_LFS_SKIP_SMUDGE: "1",
530
690
  ...sshCommand ? { GIT_SSH_COMMAND: sshCommand } : {}
531
691
  });
@@ -540,12 +700,13 @@ async function resetTempDir(dir) {
540
700
  }
541
701
  async function tryGhClone(repo, tempDir, ref) {
542
702
  let cloneTarget = repo.slug;
703
+ const host = repo.sshUrl.match(/^git@([^:]+):/)?.[1] || "github.com";
543
704
  try {
544
705
  const { stdout, stderr } = await execFileAsync("gh", [
545
706
  "auth",
546
707
  "status",
547
708
  "-h",
548
- "github.com"
709
+ host
549
710
  ], {
550
711
  timeout: 5e3,
551
712
  env: {
@@ -573,14 +734,16 @@ async function tryGhClone(repo, tempDir, ref) {
573
734
  timeout: CLONE_TIMEOUT_MS,
574
735
  env: {
575
736
  ...process.env,
576
- GIT_TERMINAL_PROMPT: "0"
737
+ GIT_TERMINAL_PROMPT: "0",
738
+ GIT_ALLOW_PROTOCOL: ALLOWED_GIT_PROTOCOLS
577
739
  }
578
740
  });
579
741
  return true;
580
742
  }
581
743
  function buildGitHubAuthError(url, repo, message) {
582
- if (repo && isGitHubSsoAuthError(message)) return `GitHub blocked HTTPS access to ${url} because the organization enforces SAML SSO.\n skills tried your existing git credentials and available fallbacks, but none succeeded.\n - Re-authorize your GitHub credentials/app for that org's SSO policy\n - Or rerun with SSH: npx skills add ${repo.sshUrl}\n - Verify access with: gh auth status -h github.com or ssh -T git@github.com`;
583
- if (repo) return `Authentication failed for ${url}.\n - For private repos, ensure you have access\n - Retry with SSH: npx skills add ${repo.sshUrl}\n - Check access with: gh auth status -h github.com or ssh -T git@github.com`;
744
+ const host = repo?.sshUrl.match(/^git@([^:]+):/)?.[1] || "github.com";
745
+ if (repo && isGitHubSsoAuthError(message)) return `GitHub blocked HTTPS access to ${url} because the organization enforces SAML SSO.\n skills tried your existing git credentials and available fallbacks, but none succeeded.\n - Re-authorize your GitHub credentials/app for that org's SSO policy\n - Or rerun with SSH: npx skills add ${repo.sshUrl}\n - Verify access with: gh auth status -h ${host} or ssh -T git@${host}`;
746
+ if (repo) return `Authentication failed for ${url}.\n - For private repos, ensure you have access\n - Retry with SSH: npx skills add ${repo.sshUrl}\n - Check access with: gh auth status -h ${host} or ssh -T git@${host}`;
584
747
  return `Authentication failed for ${url}.\n - For private repos, ensure you have access\n - For SSH: Check your keys with 'ssh -T git@github.com'\n - For HTTPS: Run 'gh auth login' or configure git credentials`;
585
748
  }
586
749
  async function cloneRepo(url, ref) {
@@ -771,6 +934,13 @@ async function addSkillToLocalLock(skillName, entry, cwd) {
771
934
  lock.skills[skillName] = entry;
772
935
  await writeLocalLock(lock, cwd);
773
936
  }
937
+ async function removeSkillFromLocalLock(skillName, cwd) {
938
+ const lock = await readLocalLock(cwd);
939
+ if (!(skillName in lock.skills)) return false;
940
+ delete lock.skills[skillName];
941
+ await writeLocalLock(lock, cwd);
942
+ return true;
943
+ }
774
944
  function createEmptyLocalLock() {
775
945
  return {
776
946
  version: CURRENT_VERSION$1,
@@ -794,9 +964,11 @@ const AGENT_PROJECT_SKILL_DIRS = [
794
964
  ".continue/skills",
795
965
  ".github/skills",
796
966
  ".goose/skills",
967
+ ".grok/skills",
797
968
  ".iflow/skills",
798
969
  ".junie/skills",
799
970
  ".kilocode/skills",
971
+ ".kimchi/skills",
800
972
  ".kiro/skills",
801
973
  ".mux/skills",
802
974
  ".neovate/skills",
@@ -830,24 +1002,44 @@ async function hasSkillMd(dir) {
830
1002
  return false;
831
1003
  }
832
1004
  }
1005
+ function warnSkippedSkill(skillMdPath, reason) {
1006
+ console.warn(`⚠ Skipped ${sanitizeMetadata(skillMdPath)} — ${stripTerminalEscapes(reason)}`);
1007
+ }
833
1008
  async function parseSkillMd(skillMdPath, options) {
1009
+ let content;
834
1010
  try {
835
- const content = await readFile(skillMdPath, "utf-8");
836
- const { data } = parseFrontmatter(content);
837
- if (!data.name || !data.description) return null;
838
- if (typeof data.name !== "string" || typeof data.description !== "string") return null;
839
- const metadata = isRecord(data.metadata) ? data.metadata : void 0;
840
- if (metadata?.internal === true && !shouldInstallInternalSkills() && !options?.includeInternal) return null;
841
- return {
842
- name: sanitizeMetadata(data.name),
843
- description: sanitizeMetadata(data.description),
844
- path: dirname(skillMdPath),
845
- rawContent: content,
846
- metadata
847
- };
848
- } catch {
1011
+ content = await readFile(skillMdPath, "utf-8");
1012
+ } catch (err) {
1013
+ warnSkippedSkill(skillMdPath, `failed to read file: ${err.message}`);
1014
+ return null;
1015
+ }
1016
+ let data;
1017
+ try {
1018
+ ({data} = parseFrontmatter(content));
1019
+ } catch (err) {
1020
+ warnSkippedSkill(skillMdPath, `YAML parse error: ${err.message}`);
1021
+ return null;
1022
+ }
1023
+ if (!data.name || !data.description) {
1024
+ const missing = [];
1025
+ if (!data.name) missing.push("name");
1026
+ if (!data.description) missing.push("description");
1027
+ warnSkippedSkill(skillMdPath, `missing required frontmatter field(s): ${missing.join(", ")}`);
849
1028
  return null;
850
1029
  }
1030
+ if (typeof data.name !== "string" || typeof data.description !== "string") {
1031
+ warnSkippedSkill(skillMdPath, `frontmatter "name" and "description" must be strings (got ${typeof data.name} and ${typeof data.description})`);
1032
+ return null;
1033
+ }
1034
+ const metadata = isRecord(data.metadata) ? data.metadata : void 0;
1035
+ if (metadata?.internal === true && !shouldInstallInternalSkills() && !options?.includeInternal) return null;
1036
+ return {
1037
+ name: sanitizeMetadata(data.name),
1038
+ description: sanitizeMetadata(data.description),
1039
+ path: dirname(skillMdPath),
1040
+ rawContent: content,
1041
+ metadata
1042
+ };
851
1043
  }
852
1044
  async function findSkillDirs(dir, depth = 0, maxDepth = 5) {
853
1045
  if (depth > maxDepth) return [];
@@ -868,6 +1060,7 @@ function isSubpathSafe(basePath, subpath) {
868
1060
  async function discoverSkills(basePath, subpath, options) {
869
1061
  const skills = [];
870
1062
  const seenNames = /* @__PURE__ */ new Set();
1063
+ const parsedSkillPaths = /* @__PURE__ */ new Set();
871
1064
  const localLock = await readLocalLock(basePath);
872
1065
  const lockedSkillNames = new Set(Object.keys(localLock.skills).map(normalizeSkillName));
873
1066
  if (subpath && !isSubpathSafe(basePath, subpath)) throw new Error(`Invalid subpath: "${subpath}" resolves outside the repository directory. Subpath must not contain ".." segments that escape the base path.`);
@@ -886,8 +1079,14 @@ async function discoverSkills(basePath, subpath, options) {
886
1079
  const directoryName = normalizeSkillName(basename(skill.path));
887
1080
  return lockedSkillNames.has(skillName) || lockedSkillNames.has(directoryName);
888
1081
  };
1082
+ const parseSkillAt = async (skillDir) => {
1083
+ const skillMdPath = resolve(skillDir, "SKILL.md");
1084
+ if (parsedSkillPaths.has(skillMdPath)) return null;
1085
+ parsedSkillPaths.add(skillMdPath);
1086
+ return parseSkillMd(skillMdPath, options);
1087
+ };
889
1088
  if (await hasSkillMd(searchPath)) {
890
- let skill = await parseSkillMd(join(searchPath, "SKILL.md"), options);
1089
+ let skill = await parseSkillAt(searchPath);
891
1090
  if (skill) {
892
1091
  if (!isInstalledProjectSkill(skill)) {
893
1092
  skill = enhanceSkill(skill);
@@ -909,7 +1108,7 @@ async function discoverSkills(basePath, subpath, options) {
909
1108
  prioritySearchDirs.push(...await getPluginSkillPaths(searchPath));
910
1109
  const tryAddSkillAt = async (skillDir) => {
911
1110
  if (!await hasSkillMd(skillDir)) return false;
912
- let skill = await parseSkillMd(join(skillDir, "SKILL.md"), options);
1111
+ let skill = await parseSkillAt(skillDir);
913
1112
  if (!skill || seenNames.has(skill.name)) return true;
914
1113
  if (isInstalledProjectSkill(skill)) return true;
915
1114
  skill = enhanceSkill(skill);
@@ -939,7 +1138,7 @@ async function discoverSkills(basePath, subpath, options) {
939
1138
  if (skills.length === 0 || options?.fullDepth) {
940
1139
  const allSkillDirs = await findSkillDirs(searchPath);
941
1140
  for (const skillDir of allSkillDirs) {
942
- let skill = await parseSkillMd(join(skillDir, "SKILL.md"), options);
1141
+ let skill = await parseSkillAt(skillDir);
943
1142
  if (skill && !seenNames.has(skill.name) && !isInstalledProjectSkill(skill)) {
944
1143
  skill = enhanceSkill(skill);
945
1144
  skills.push(skill);
@@ -967,6 +1166,7 @@ const claudeHome = process.env.CLAUDE_CONFIG_DIR?.trim() || join(home, ".claude"
967
1166
  const vibeHome = process.env.VIBE_HOME?.trim() || join(home, ".vibe");
968
1167
  const hermesHome = process.env.HERMES_HOME?.trim() || join(home, ".hermes");
969
1168
  const autohandHome = process.env.AUTOHAND_HOME?.trim() || join(home, ".autohand");
1169
+ const grokHome = process.env.GROK_HOME?.trim() || join(home, ".grok");
970
1170
  const zedAppDataHome = process.env.APPDATA?.trim();
971
1171
  const zedFlatpakConfigHome = process.env.FLATPAK_XDG_CONFIG_HOME?.trim();
972
1172
  function packageJsonHasDependency(packageJsonPath, dependencyName) {
@@ -986,6 +1186,9 @@ function getOpenClawGlobalSkillsDir(homeDir = home, pathExists = existsSync) {
986
1186
  function isZCodeInstalled(homeDir = home, pathExists = existsSync) {
987
1187
  return pathExists(join(homeDir, ".zcode")) || pathExists("/Applications/ZCode.app");
988
1188
  }
1189
+ function isKimchiInstalled(homeDir = home, pathExists = existsSync) {
1190
+ return pathExists(join(homeDir, ".config", "kimchi"));
1191
+ }
989
1192
  const agents = {
990
1193
  "aider-desk": {
991
1194
  name: "aider-desk",
@@ -1269,6 +1472,15 @@ const agents = {
1269
1472
  return existsSync(join(configHome, "goose"));
1270
1473
  }
1271
1474
  },
1475
+ grok: {
1476
+ name: "grok",
1477
+ displayName: "Grok Build",
1478
+ skillsDir: ".grok/skills",
1479
+ globalSkillsDir: join(grokHome, "skills"),
1480
+ detectInstalled: async () => {
1481
+ return existsSync(grokHome);
1482
+ }
1483
+ },
1272
1484
  "hermes-agent": {
1273
1485
  name: "hermes-agent",
1274
1486
  displayName: "Hermes Agent",
@@ -1323,6 +1535,15 @@ const agents = {
1323
1535
  return existsSync(join(home, ".kilocode"));
1324
1536
  }
1325
1537
  },
1538
+ kimchi: {
1539
+ name: "kimchi",
1540
+ displayName: "Kimchi",
1541
+ skillsDir: ".kimchi/skills",
1542
+ globalSkillsDir: join(home, ".config", "kimchi", "harness", "skills"),
1543
+ detectInstalled: async () => {
1544
+ return isKimchiInstalled();
1545
+ }
1546
+ },
1326
1547
  "kimi-code-cli": {
1327
1548
  name: "kimi-code-cli",
1328
1549
  displayName: "Kimi Code CLI",
@@ -2118,7 +2339,7 @@ async function installBlobSkillForAgent(skill, agentType, options = {}) {
2118
2339
  mode: "symlink"
2119
2340
  };
2120
2341
  if (!isGlobal && !isUniversalAgent(agentType)) {
2121
- if (!existsSync(join(cwd, agents[agentType].skillsDir.split("/")[0]))) return {
2342
+ if (!existsSync(join(cwd, agents[agentType].skillsDir.split("/")[0])) && agentType !== "claude-code") return {
2122
2343
  success: true,
2123
2344
  path: canonicalDir,
2124
2345
  canonicalPath: canonicalDir,
@@ -3039,9 +3260,11 @@ const PRIORITY_PREFIXES = [
3039
3260
  ".continue/skills/",
3040
3261
  ".github/skills/",
3041
3262
  ".goose/skills/",
3263
+ ".grok/skills/",
3042
3264
  ".iflow/skills/",
3043
3265
  ".junie/skills/",
3044
3266
  ".kilocode/skills/",
3267
+ ".kimchi/skills/",
3045
3268
  ".kiro/skills/",
3046
3269
  ".mux/skills/",
3047
3270
  ".neovate/skills/",
@@ -3208,7 +3431,7 @@ async function tryBlobInstall(ownerRepo, options = {}) {
3208
3431
  tree
3209
3432
  };
3210
3433
  }
3211
- var version$1 = "1.5.18";
3434
+ var version$1 = "1.5.20";
3212
3435
  const isCancelled$1 = (value) => typeof value === "symbol";
3213
3436
  const EVE_AGENT_LABEL = "eve agent";
3214
3437
  async function isSourcePrivate(source) {
@@ -3759,6 +3982,7 @@ async function runAdd(args, options = {}) {
3759
3982
  spinner$3.stop(`Source: ${parsed.type === "local" ? parsed.localPath : parsed.url}${parsed.ref ? ` @ ${import_picocolors.default.yellow(parsed.ref)}` : ""}${parsed.subpath ? ` (${parsed.subpath})` : ""}${parsed.skillFilter ? ` ${import_picocolors.default.dim("@")}${import_picocolors.default.cyan(parsed.skillFilter)}` : ""}`);
3760
3983
  const ownerRepoRaw = getOwnerRepo(parsed);
3761
3984
  const repoPrivacyPromise = (() => {
3985
+ if (parsed.type !== "github") return Promise.resolve(null);
3762
3986
  if (!ownerRepoRaw) return Promise.resolve(null);
3763
3987
  const ownerRepo = parseOwnerRepo(ownerRepoRaw);
3764
3988
  if (!ownerRepo) return Promise.resolve(null);
@@ -3900,34 +4124,24 @@ async function runAdd(args, options = {}) {
3900
4124
  return getSkillDisplayName(a).localeCompare(getSkillDisplayName(b));
3901
4125
  });
3902
4126
  const hasGroups = sortedSkills.some((s) => s.pluginName);
3903
- let selected;
3904
- if (hasGroups) {
3905
- const kebabToTitle = (s) => s.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
3906
- const grouped = {};
3907
- for (const s of sortedSkills) {
3908
- const groupName = s.pluginName ? kebabToTitle(s.pluginName) : "Other";
3909
- if (!grouped[groupName]) grouped[groupName] = [];
3910
- grouped[groupName].push({
3911
- value: s,
3912
- label: getSkillDisplayName(s),
3913
- hint: s.description.length > 60 ? s.description.slice(0, 57) + "…" : s.description
3914
- });
3915
- }
3916
- selected = await groupMultiselect({
3917
- message: `Select skills to install ${import_picocolors.default.dim("(space to toggle)")}`,
3918
- options: grouped,
3919
- required: true
3920
- });
3921
- } else selected = await multiselect({
3922
- message: "Select skills to install",
3923
- options: sortedSkills.map((s) => ({
3924
- value: s,
3925
- label: getSkillDisplayName(s),
3926
- hint: s.description.length > 60 ? s.description.slice(0, 57) + "…" : s.description
3927
- })),
3928
- required: true
4127
+ const kebabToTitle = (s) => s.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
4128
+ const skillChoices = sortedSkills.map((s) => ({
4129
+ value: s,
4130
+ label: getSkillDisplayName(s),
4131
+ group: hasGroups ? s.pluginName ? kebabToTitle(s.pluginName) : "Other" : void 0,
4132
+ detail: s.description
4133
+ }));
4134
+ const selected = await searchMultiselect({
4135
+ message: hasGroups ? `Select skills to install ${import_picocolors.default.dim("(space to toggle)")}` : "Select skills to install",
4136
+ items: skillChoices,
4137
+ required: true,
4138
+ maxVisible: 20,
4139
+ searchable: !hasGroups,
4140
+ showDetail: true,
4141
+ showSelectedSummary: false,
4142
+ selectGroups: hasGroups
3929
4143
  });
3930
- if (isCancel(selected)) {
4144
+ if (isCancelled$1(selected)) {
3931
4145
  cancel("Installation cancelled");
3932
4146
  await cleanup(tempDir);
3933
4147
  process.exit(0);
@@ -5039,7 +5253,7 @@ function deriveSkillFolder(skillPath) {
5039
5253
  return folder;
5040
5254
  }
5041
5255
  function supportsAppendedSubpath(source) {
5042
- if (source.startsWith("git@")) return false;
5256
+ if (source.startsWith("git@") || source.startsWith("ssh://")) return false;
5043
5257
  if (source.endsWith(".git")) return false;
5044
5258
  if (source.startsWith("http://") || source.startsWith("https://")) try {
5045
5259
  const host = new URL(source).hostname;
@@ -5057,6 +5271,11 @@ function getLocalSource(entry) {
5057
5271
  if ((entry.sourceType === "git" || entry.sourceType === "gitlab") && isBareShorthand(entry.source)) return null;
5058
5272
  return entry.source;
5059
5273
  }
5274
+ function shouldUseFullDepthForUpdate(entry) {
5275
+ if (!entry.skillPath) return false;
5276
+ const source = entry.sourceType && entry.sourceType !== "github" ? getLocalSource(entry) : entry.source;
5277
+ return source !== null && !supportsAppendedSubpath(source);
5278
+ }
5060
5279
  function appendFolderAndRef(source, skillPath, ref) {
5061
5280
  if (!supportsAppendedSubpath(source)) return formatSourceInput(source, ref);
5062
5281
  const folder = deriveSkillFolder(skillPath);
@@ -5180,18 +5399,26 @@ async function runList(args) {
5180
5399
  global: scope,
5181
5400
  agentFilter
5182
5401
  });
5402
+ const cwd = process.cwd();
5403
+ const lockedSkills = scope ? await getAllLockedSkills() : (await readLocalLock(cwd)).skills;
5404
+ const lockEntriesBySanitizedName = new Map(Object.entries(lockedSkills).map(([name, entry]) => [sanitizeName(name), entry]));
5405
+ const getLockEntry = (skillName) => lockedSkills[skillName] ?? lockEntriesBySanitizedName.get(sanitizeName(skillName));
5183
5406
  if (options.json) {
5184
- const jsonOutput = installedSkills.map((skill) => ({
5185
- name: skill.name,
5186
- path: skill.canonicalPath,
5187
- scope: skill.scope,
5188
- agents: skill.agents.map((a) => agents[a].displayName)
5189
- }));
5407
+ const jsonOutput = installedSkills.map((skill) => {
5408
+ const lockEntry = getLockEntry(skill.name);
5409
+ return {
5410
+ name: skill.name,
5411
+ path: skill.canonicalPath,
5412
+ scope: skill.scope,
5413
+ agents: skill.agents.map((a) => agents[a].displayName),
5414
+ source: lockEntry?.source ?? null,
5415
+ sourceUrl: lockEntry?.sourceUrl ?? null,
5416
+ sourceType: lockEntry?.sourceType ?? null
5417
+ };
5418
+ });
5190
5419
  console.log(JSON.stringify(jsonOutput, null, 2));
5191
5420
  return;
5192
5421
  }
5193
- const lockedSkills = await getAllLockedSkills();
5194
- const cwd = process.cwd();
5195
5422
  const scopeLabel = scope ? "Global" : "Project";
5196
5423
  if (installedSkills.length === 0) {
5197
5424
  if (options.json) {
@@ -5210,14 +5437,17 @@ async function runList(args) {
5210
5437
  const agentInfo = skill.agents.length > 0 ? formatList(agentNames) : `${YELLOW}not linked${RESET$2}`;
5211
5438
  const paddedName = sanitizeMetadata(skill.name).padEnd(maxNameLength);
5212
5439
  const paddedPath = shortPath.padEnd(maxPathLength);
5213
- console.log(`${prefix}${CYAN}${paddedName}${RESET$2} ${DIM$2}${paddedPath}${RESET$2} ${DIM$2}Agents:${RESET$2} ${agentInfo}`);
5440
+ const source = getLockEntry(skill.name)?.source ?? null;
5441
+ const sourceLabel = source ? sanitizeMetadata(source) : "local";
5442
+ console.log(`${prefix}${CYAN}${paddedName}${RESET$2} ${DIM$2}${paddedPath}${RESET$2}`);
5443
+ console.log(`${prefix} ${DIM$2}Agents:${RESET$2} ${agentInfo} ${DIM$2}Source:${RESET$2} ${sourceLabel}`);
5214
5444
  }
5215
5445
  console.log(`${BOLD$2}${scopeLabel} Skills${RESET$2}`);
5216
5446
  console.log();
5217
5447
  const groupedSkills = {};
5218
5448
  const ungroupedSkills = [];
5219
5449
  for (const skill of installedSkills) {
5220
- const lockEntry = lockedSkills[skill.name];
5450
+ const lockEntry = getLockEntry(skill.name);
5221
5451
  if (lockEntry?.pluginName) {
5222
5452
  const group = lockEntry.pluginName;
5223
5453
  if (!groupedSkills[group]) groupedSkills[group] = [];
@@ -5269,6 +5499,17 @@ async function runList(args) {
5269
5499
  console.log();
5270
5500
  }
5271
5501
  }
5502
+ function resolveSkillsToRemove(requested, folderNames, lockKeys = []) {
5503
+ const identityBySanitized = /* @__PURE__ */ new Map();
5504
+ for (const folder of folderNames) identityBySanitized.set(sanitizeName(folder), folder);
5505
+ for (const key of lockKeys) identityBySanitized.set(sanitizeName(key), key);
5506
+ const matched = /* @__PURE__ */ new Set();
5507
+ for (const name of requested) {
5508
+ const hit = identityBySanitized.get(sanitizeName(name));
5509
+ if (hit) matched.add(hit);
5510
+ }
5511
+ return Array.from(matched);
5512
+ }
5272
5513
  async function removeCommand(skillNames, options) {
5273
5514
  const agentResult = await detectAgent();
5274
5515
  if (agentResult.isAgent) {
@@ -5298,7 +5539,10 @@ async function removeCommand(skillNames, options) {
5298
5539
  }
5299
5540
  const installedSkills = Array.from(skillNamesSet).sort();
5300
5541
  spinner$1.stop(`Found ${installedSkills.length} unique installed skill(s)`);
5301
- if (installedSkills.length === 0) {
5542
+ const lockSkillsKeys = isGlobal ? Object.keys((await readSkillLock()).skills) : Object.keys((await readLocalLock(cwd)).skills);
5543
+ const requestedSkills = options.all ? [...installedSkills, ...lockSkillsKeys] : skillNames;
5544
+ const resolvedRequestedSkills = options.all || skillNames.length > 0 ? resolveSkillsToRemove(requestedSkills, installedSkills, lockSkillsKeys) : [];
5545
+ if (installedSkills.length === 0 && resolvedRequestedSkills.length === 0) {
5302
5546
  outro(import_picocolors.default.yellow("No skills found to remove."));
5303
5547
  return;
5304
5548
  }
@@ -5312,9 +5556,9 @@ async function removeCommand(skillNames, options) {
5312
5556
  }
5313
5557
  }
5314
5558
  let selectedSkills = [];
5315
- if (options.all) selectedSkills = installedSkills;
5559
+ if (options.all) selectedSkills = resolvedRequestedSkills;
5316
5560
  else if (skillNames.length > 0) {
5317
- selectedSkills = installedSkills.filter((s) => skillNames.some((name) => name.toLowerCase() === s.toLowerCase()));
5561
+ selectedSkills = resolvedRequestedSkills;
5318
5562
  if (selectedSkills.length === 0) {
5319
5563
  log.error(`No matching skills found for: ${skillNames.join(", ")}`);
5320
5564
  return;
@@ -5333,7 +5577,7 @@ async function removeCommand(skillNames, options) {
5333
5577
  cancel("Removal cancelled");
5334
5578
  process.exit(0);
5335
5579
  }
5336
- selectedSkills = selected;
5580
+ selectedSkills = resolveSkillsToRemove(selected, installedSkills, lockSkillsKeys);
5337
5581
  }
5338
5582
  let targetAgents;
5339
5583
  if (options.agent && options.agent.length > 0) targetAgents = options.agent;
@@ -5397,10 +5641,19 @@ async function removeCommand(skillNames, options) {
5397
5641
  recursive: true,
5398
5642
  force: true
5399
5643
  });
5400
- const lockEntry = isGlobal ? await getSkillFromLock(skillName) : null;
5401
- const effectiveSource = lockEntry?.source || "local";
5402
- const effectiveSourceType = lockEntry?.sourceType || "local";
5403
- if (isGlobal) await removeSkillFromLock(skillName);
5644
+ let effectiveSource = "local";
5645
+ let effectiveSourceType = "local";
5646
+ if (isGlobal) {
5647
+ const lockEntry = await getSkillFromLock(skillName);
5648
+ effectiveSource = lockEntry?.source || "local";
5649
+ effectiveSourceType = lockEntry?.sourceType || "local";
5650
+ await removeSkillFromLock(skillName);
5651
+ } else {
5652
+ const lockEntry = (await readLocalLock(cwd)).skills[skillName];
5653
+ effectiveSource = lockEntry?.source || "local";
5654
+ effectiveSourceType = lockEntry?.sourceType || "local";
5655
+ await removeSkillFromLocalLock(skillName, cwd);
5656
+ }
5404
5657
  results.push({
5405
5658
  skill: skillName,
5406
5659
  success: true,
@@ -5670,7 +5923,7 @@ async function updateGlobalSkills(options = {}) {
5670
5923
  console.log(` ${DIM$1}✗ Failed to fetch tree for ${source}${RESET$1}`);
5671
5924
  continue;
5672
5925
  }
5673
- const discoveredPaths = findSkillMdPaths(tree);
5926
+ const discoveredPaths = tree.tree.filter((entry) => entry.type === "blob").map((entry) => entry.path);
5674
5927
  const deletedSkills = await checkAndPromptForDeletions(source, Object.entries(lock.skills).filter(([_, entry]) => entry.source === source).map(([name, _]) => name), lock.skills, true, options, discoveredPaths);
5675
5928
  const deletedSkillSet = new Set(deletedSkills);
5676
5929
  for (const { name: skillName, entry } of itemsForSource) {
@@ -5685,7 +5938,7 @@ async function updateGlobalSkills(options = {}) {
5685
5938
  continue;
5686
5939
  }
5687
5940
  tempDir = await cloneRepo(sourceUrl, firstEntry.ref);
5688
- const discoveredPaths = (await discoverSkills(tempDir)).map((skill) => {
5941
+ const discoveredPaths = (await discoverSkills(tempDir, void 0, { fullDepth: true })).map((skill) => {
5689
5942
  return join(relative(tempDir, skill.path), "SKILL.md").split(sep).join("/");
5690
5943
  });
5691
5944
  const deletedSkills = await checkAndPromptForDeletions(source, Object.entries(lock.skills).filter(([_, entry]) => entry.source === source).map(([name, _]) => name), lock.skills, true, options, discoveredPaths);
@@ -5750,10 +6003,12 @@ async function updateGlobalSkills(options = {}) {
5750
6003
  console.log(` ${DIM$1}✗ Failed to update ${safeName}: CLI entrypoint not found at ${cliEntry}${RESET$1}`);
5751
6004
  continue;
5752
6005
  }
6006
+ const fullDepthArgs = shouldUseFullDepthForUpdate(update.entry) ? ["--full-depth"] : [];
5753
6007
  if (spawnSync(process.execPath, [
5754
6008
  cliEntry,
5755
6009
  "add",
5756
6010
  installUrl,
6011
+ ...fullDepthArgs,
5757
6012
  "-g",
5758
6013
  "-y"
5759
6014
  ], {
@@ -5851,7 +6106,7 @@ async function updateProjectSkills(options = {}) {
5851
6106
  }
5852
6107
  try {
5853
6108
  tempDir = await cloneRepo(sourceUrl, ref);
5854
- const discoveredPaths = (await discoverSkills(tempDir)).map((s) => {
6109
+ const discoveredPaths = (await discoverSkills(tempDir, void 0, { fullDepth: true })).map((s) => {
5855
6110
  return join(relative(tempDir, s.path), "SKILL.md").split(sep).join("/");
5856
6111
  });
5857
6112
  deletedSkills = await checkAndPromptForDeletions(source, allLockedForSource, localLock.skills, false, options, discoveredPaths);
@@ -5871,6 +6126,7 @@ async function updateProjectSkills(options = {}) {
5871
6126
  continue;
5872
6127
  }
5873
6128
  const subagentArgs = skill.entry.subagents?.length ? ["--subagent", ...skill.entry.subagents.map((s) => s === "" ? "root" : s)] : [];
6129
+ const fullDepthArgs = shouldUseFullDepthForUpdate(skill.entry) ? ["--full-depth"] : [];
5874
6130
  if (spawnSync(process.execPath, [
5875
6131
  cliEntry,
5876
6132
  "add",
@@ -5878,6 +6134,7 @@ async function updateProjectSkills(options = {}) {
5878
6134
  "--skill",
5879
6135
  skill.name,
5880
6136
  ...subagentArgs,
6137
+ ...fullDepthArgs,
5881
6138
  "-y"
5882
6139
  ], {
5883
6140
  stdio: [
@@ -6558,6 +6815,11 @@ async function main() {
6558
6815
  }
6559
6816
  const command = args[0];
6560
6817
  const restArgs = args.slice(1);
6818
+ if (command !== "--help" && command !== "-h" && command !== "--version" && command !== "-v" && (restArgs.includes("--help") || restArgs.includes("-h"))) {
6819
+ if (command === "remove" || command === "rm" || command === "r") showRemoveHelp();
6820
+ else showHelp();
6821
+ return;
6822
+ }
6561
6823
  switch (command) {
6562
6824
  case "find":
6563
6825
  case "search":
@@ -6597,14 +6859,11 @@ async function main() {
6597
6859
  }
6598
6860
  case "remove":
6599
6861
  case "rm":
6600
- case "r":
6601
- if (restArgs.includes("--help") || restArgs.includes("-h")) {
6602
- showRemoveHelp();
6603
- break;
6604
- }
6862
+ case "r": {
6605
6863
  const { skills, options: removeOptions } = parseRemoveOptions(restArgs);
6606
6864
  await removeCommand(skills, removeOptions);
6607
6865
  break;
6866
+ }
6608
6867
  case "experimental_sync": {
6609
6868
  if (!inAgent) showLogo();
6610
6869
  const { options: syncOptions } = parseSyncOptions(restArgs);
@@ -6631,6 +6890,7 @@ async function main() {
6631
6890
  default:
6632
6891
  console.log(`Unknown command: ${command}`);
6633
6892
  console.log(`Run ${BOLD}skills --help${RESET} for usage.`);
6893
+ process.exitCode = 1;
6634
6894
  }
6635
6895
  }
6636
6896
  main().finally(() => flushTelemetry().then(() => process.exit(process.exitCode ?? 0)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skills",
3
- "version": "1.5.18",
3
+ "version": "1.5.20",
4
4
  "description": "The open agent skills ecosystem",
5
5
  "type": "module",
6
6
  "bin": {
@@ -67,12 +67,14 @@
67
67
  "gemini-cli",
68
68
  "github-copilot",
69
69
  "goose",
70
+ "grok",
70
71
  "hermes-agent",
71
72
  "inference-sh",
72
73
  "jazz",
73
74
  "junie",
74
75
  "iflow-cli",
75
76
  "kilo",
77
+ "kimchi",
76
78
  "kimi-code-cli",
77
79
  "kiro-cli",
78
80
  "kode",
@@ -136,7 +138,7 @@
136
138
  "xdg-basedir": "^5.1.0"
137
139
  },
138
140
  "engines": {
139
- "node": ">=18"
141
+ "node": ">=22.20.0"
140
142
  },
141
143
  "packageManager": "pnpm@10.17.1",
142
144
  "dependencies": {