skilllib 1.12.2 → 1.14.0

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.
@@ -139,6 +139,77 @@ import { join as join6, resolve as resolve4 } from "path";
139
139
  // src/harnesses.ts
140
140
  import { existsSync } from "fs";
141
141
  import { delimiter, join as join2 } from "path";
142
+ var EXTRAS = [
143
+ { id: "github-copilot", name: "GitHub Copilot", short: "Cp", icon: "\u25D0", color: "#E8E8E8", project: [".agents/skills", ".claude/skills", ".github/skills"], global: [".agents/skills", ".claude/skills", ".copilot/skills"], detect: [".copilot"], bin: "copilot" },
144
+ { id: "gemini-cli", name: "Gemini CLI", short: "Ge", icon: "\u2726", color: "#8AB4F8", project: [".agents/skills", ".gemini/skills"], global: [".agents/skills", ".gemini/skills"], detect: [], bin: "gemini" },
145
+ { id: "opencode", name: "OpenCode", short: "Oc", icon: "\u2318", color: "#A78BFA", project: [".agents/skills", ".claude/skills", ".opencode/skills"], global: [".agents/skills", ".claude/skills", ".config/opencode/skills"], detect: [".config/opencode", ".opencode"], bin: "opencode" },
146
+ { id: "antigravity", name: "Antigravity", short: "Ag", icon: "\u25B2", color: "#34A853", project: [".agents/skills", ".agent/skills"], global: [".gemini/config/skills", ".gemini/antigravity/skills"], detect: [".gemini/antigravity"], bin: "agy", app: "Antigravity" },
147
+ { id: "antigravity-ide", name: "Antigravity IDE", short: "Ai", icon: "\u25B2", color: "#34A853", project: [".agents/skills", ".agent/skills"], global: [".gemini/antigravity-ide/skills"], detect: [".gemini/antigravity-ide"], app: "Antigravity IDE" },
148
+ { id: "antigravity-cli", name: "Antigravity CLI", short: "Ac", icon: "\u25B2", color: "#34A853", project: [".agents/skills", ".agent/skills"], global: [".gemini/antigravity-cli/skills"], detect: [".gemini/antigravity-cli"], bin: "agy" },
149
+ { id: "windsurf", name: "Windsurf", short: "Ws", icon: "\u224B", color: "#0EA5E9", project: [".windsurf/skills"], global: [".codeium/windsurf/skills"], detect: [".codeium/windsurf"], bin: "windsurf", app: "Windsurf" },
150
+ { id: "devin", name: "Devin", short: "Dv", icon: "\u0394", color: "#7C6AF7", project: [".agents/skills", ".devin/skills", ".windsurf/skills"], global: [".agents/skills", ".config/devin/skills"], detect: [".config/devin"], bin: "devin" },
151
+ { id: "cline", name: "Cline", short: "Cl", icon: "\u25A3", color: "#22C55E", project: [".agents/skills"], global: [".agents/skills"], detect: [".cline"], bin: "cline" },
152
+ { id: "amp", name: "Amp", short: "Am", icon: "\u26A1", color: "#F5C16C", project: [".agents/skills"], global: [".config/agents/skills"], detect: [".config/amp"], bin: "amp" },
153
+ { id: "droid", name: "Factory Droid", short: "Dr", icon: "\xB7", color: "#8A93A6", project: [".agents/skills"], global: [".agents/skills", ".factory/skills"], detect: [".factory"], bin: "droid" },
154
+ { id: "openclaw", name: "OpenClaw", short: "Ow", icon: "\xB7", color: "#8A93A6", project: ["skills"], global: [".openclaw/skills"], detect: [".openclaw", ".clawdbot", ".moltbot"], bin: "openclaw" },
155
+ { id: "continue", name: "Continue", short: "Cn", icon: "\xB7", color: "#8A93A6", project: [".continue/skills"], global: [".continue/skills"], detect: [".continue"] },
156
+ { id: "augment", name: "Augment", short: "Au", icon: "\xB7", color: "#8A93A6", project: [".augment/skills"], global: [".augment/skills"], detect: [".augment"] },
157
+ { id: "junie", name: "Junie", short: "Ju", icon: "\xB7", color: "#8A93A6", project: [".junie/skills"], global: [".junie/skills"], detect: [".junie"], bin: "junie" },
158
+ { id: "roo", name: "Roo Code", short: "Ro", icon: "\xB7", color: "#8A93A6", project: [".roo/skills"], global: [".roo/skills"], detect: [".roo"] },
159
+ { id: "goose", name: "Goose", short: "Gs", icon: "\xB7", color: "#8A93A6", project: [".goose/skills"], global: [".config/goose/skills"], detect: [".config/goose"], bin: "goose" },
160
+ { id: "openhands", name: "OpenHands", short: "Oh", icon: "\xB7", color: "#8A93A6", project: [".openhands/skills"], global: [".openhands/skills"], detect: [".openhands"] },
161
+ { id: "crush", name: "Crush", short: "Cr", icon: "\xB7", color: "#8A93A6", project: [".crush/skills"], global: [".config/crush/skills"], detect: [".config/crush"], bin: "crush" },
162
+ { id: "pi", name: "Pi", short: "Pi", icon: "\xB7", color: "#8A93A6", project: [".agents/skills"], global: [".agents/skills", ".pi/agent/skills"], detect: [".pi/agent"], bin: "pi" },
163
+ { id: "kimi-code-cli", name: "Kimi Code CLI", short: "Ki", icon: "\xB7", color: "#8A93A6", project: [".agents/skills"], global: [".agents/skills"], detect: [".kimi"], bin: "kimi" },
164
+ { id: "kilo", name: "Kilo Code", short: "Kl", icon: "\xB7", color: "#8A93A6", project: [".agents/skills"], global: [".kilo/skills", ".kilocode/skills"], detect: [".kilo", ".kilocode"] },
165
+ { id: "kiro-cli", name: "Kiro CLI", short: "Kr", icon: "\xB7", color: "#8A93A6", project: [".kiro/skills"], global: [".kiro/skills"], detect: [".kiro"], bin: "kiro" },
166
+ { id: "qwen-code", name: "Qwen Code", short: "Qw", icon: "\xB7", color: "#8A93A6", project: [".qwen/skills"], global: [".qwen/skills"], detect: [".qwen"], bin: "qwen" },
167
+ { id: "warp", name: "Warp", short: "Wp", icon: "\xB7", color: "#8A93A6", project: [".agents/skills"], global: [".agents/skills"], detect: [".warp"], app: "Warp" },
168
+ { id: "replit", name: "Replit", short: "Rp", icon: "\xB7", color: "#8A93A6", project: [".agents/skills"], global: [".config/agents/skills"], detect: [] },
169
+ { id: "trae", name: "Trae", short: "Tr", icon: "\xB7", color: "#8A93A6", project: [".trae/skills"], global: [".trae/skills"], detect: [".trae"], app: "Trae" },
170
+ { id: "mux", name: "Mux", short: "Mx", icon: "\xB7", color: "#8A93A6", project: [".mux/skills"], global: [".mux/skills"], detect: [".mux"] },
171
+ { id: "mistral-vibe", name: "Mistral Vibe", short: "Mv", icon: "\xB7", color: "#8A93A6", project: [".vibe/skills"], global: [".vibe/skills"], detect: [".vibe"], bin: "vibe" },
172
+ { id: "qoder", name: "Qoder", short: "Qd", icon: "\xB7", color: "#8A93A6", project: [".qoder/skills"], global: [".qoder/skills"], detect: [".qoder"] },
173
+ { id: "zencoder", name: "Zencoder", short: "Ze", icon: "\xB7", color: "#8A93A6", project: [".zencoder/skills"], global: [".zencoder/skills"], detect: [".zencoder"] },
174
+ { id: "neovate", name: "Neovate", short: "Nv", icon: "\xB7", color: "#8A93A6", project: [".neovate/skills"], global: [".neovate/skills"], detect: [".neovate"] },
175
+ { id: "adal", name: "AdaL", short: "Ad", icon: "\xB7", color: "#8A93A6", project: [".adal/skills"], global: [".adal/skills"], detect: [".adal"] },
176
+ { id: "bob", name: "IBM Bob", short: "Bb", icon: "\xB7", color: "#8A93A6", project: [".bob/skills"], global: [".bob/skills"], detect: [".bob"] },
177
+ { id: "aider-desk", name: "AiderDesk", short: "Adk", icon: "\xB7", color: "#8A93A6", project: [".aider-desk/skills"], global: [".aider-desk/skills"], detect: [".aider-desk"] },
178
+ { id: "codebuddy", name: "CodeBuddy", short: "Cb", icon: "\xB7", color: "#8A93A6", project: [".codebuddy/skills"], global: [".codebuddy/skills"], detect: [".codebuddy"] },
179
+ { id: "command-code", name: "Command Code", short: "Cm", icon: "\xB7", color: "#8A93A6", project: [".commandcode/skills"], global: [".commandcode/skills"], detect: [".commandcode"] },
180
+ { id: "cortex", name: "Cortex Code", short: "Ct", icon: "\xB7", color: "#8A93A6", project: [".cortex/skills"], global: [".snowflake/cortex/skills"], detect: [".snowflake/cortex"] },
181
+ { id: "hermes-agent", name: "Hermes Agent", short: "He", icon: "\xB7", color: "#8A93A6", project: [".hermes/skills"], global: [".hermes/skills"], detect: [".hermes"] },
182
+ { id: "iflow-cli", name: "iFlow CLI", short: "If", icon: "\xB7", color: "#8A93A6", project: [".iflow/skills"], global: [".iflow/skills"], detect: [".iflow"] },
183
+ { id: "kode", name: "Kode", short: "Kd", icon: "\xB7", color: "#8A93A6", project: [".kode/skills"], global: [".kode/skills"], detect: [".kode"] },
184
+ { id: "mcpjam", name: "MCPJam", short: "Mj", icon: "\xB7", color: "#8A93A6", project: [".mcpjam/skills"], global: [".mcpjam/skills"], detect: [".mcpjam"] },
185
+ { id: "minimax-code", name: "MiniMax Code", short: "Mm", icon: "\xB7", color: "#8A93A6", project: [".minimax/skills"], global: [".minimax/skills"], detect: [".minimax"] },
186
+ { id: "pochi", name: "Pochi", short: "Po", icon: "\xB7", color: "#8A93A6", project: [".pochi/skills"], global: [".pochi/skills"], detect: [".pochi"] },
187
+ { id: "rovodev", name: "Rovo Dev", short: "Rv", icon: "\xB7", color: "#8A93A6", project: [".rovodev/skills"], global: [".rovodev/skills"], detect: [".rovodev"] },
188
+ { id: "tabnine-cli", name: "Tabnine CLI", short: "Tn", icon: "\xB7", color: "#8A93A6", project: [".tabnine/agent/skills"], global: [".tabnine/agent/skills"], detect: [".tabnine"] },
189
+ { id: "zcode", name: "ZCode", short: "Zc", icon: "\xB7", color: "#8A93A6", project: [".zcode/skills"], global: [".zcode/skills"], detect: [".zcode"], app: "ZCode" },
190
+ { id: "autohand-code", name: "Autohand Code", short: "Ah", icon: "\xB7", color: "#8A93A6", project: [".autohand/skills"], global: [".autohand/skills"], detect: [".autohand"] },
191
+ { id: "codearts-agent", name: "CodeArts Agent", short: "Ca", icon: "\xB7", color: "#8A93A6", project: [".codeartsdoer/skills"], global: [".codeartsdoer/skills"], detect: [".codeartsdoer"] },
192
+ { id: "codemaker", name: "CodeMaker", short: "Mk", icon: "\xB7", color: "#8A93A6", project: [".codemaker/skills"], global: [".codemaker/skills"], detect: [".codemaker"] },
193
+ { id: "codestudio", name: "CodeStudio", short: "Cs", icon: "\xB7", color: "#8A93A6", project: [".codestudio/skills"], global: [".codestudio/skills"], detect: [".codestudio"] },
194
+ { id: "deepagents", name: "Deep Agents", short: "Da", icon: "\xB7", color: "#8A93A6", project: [".agents/skills"], global: [".deepagents/agent/skills"], detect: [".deepagents"] },
195
+ { id: "dexto", name: "Dexto", short: "Dx", icon: "\xB7", color: "#8A93A6", project: [".agents/skills"], global: [".agents/skills"], detect: [".dexto"] },
196
+ { id: "firebender", name: "Firebender", short: "Fb", icon: "\xB7", color: "#8A93A6", project: [".agents/skills"], global: [".firebender/skills"], detect: [".firebender"] },
197
+ { id: "forgecode", name: "ForgeCode", short: "Fc", icon: "\xB7", color: "#8A93A6", project: [".forge/skills"], global: [".forge/skills"], detect: [".forge"] },
198
+ { id: "fx", name: "fx", short: "Fx", icon: "\xB7", color: "#8A93A6", project: [".agents/skills"], global: [".agents/skills"], detect: [".fx"] },
199
+ { id: "inference-sh", name: "inference.sh", short: "In", icon: "\xB7", color: "#8A93A6", project: [".inferencesh/skills"], global: [".inferencesh/skills"], detect: [".inferencesh"] },
200
+ { id: "jazz", name: "Jazz", short: "Jz", icon: "\xB7", color: "#8A93A6", project: [".jazz/skills"], global: [".jazz/skills"], detect: [".jazz"] },
201
+ { id: "kimchi", name: "Kimchi", short: "Kc", icon: "\xB7", color: "#8A93A6", project: [".kimchi/skills"], global: [".config/kimchi/harness/skills"], detect: [".config/kimchi"] },
202
+ { id: "lingma", name: "Lingma", short: "Lg", icon: "\xB7", color: "#8A93A6", project: [".lingma/skills"], global: [".lingma/skills"], detect: [".lingma"] },
203
+ { id: "loaf", name: "Loaf", short: "Lf", icon: "\xB7", color: "#8A93A6", project: [".agents/skills"], global: [".agents/skills"], detect: [".loaf"] },
204
+ { id: "moxby", name: "Moxby", short: "Mb", icon: "\xB7", color: "#8A93A6", project: [".moxby/skills"], global: [".moxby/skills"], detect: [".moxby"] },
205
+ { id: "ona", name: "Ona", short: "On", icon: "\xB7", color: "#8A93A6", project: [".ona/skills"], global: [".ona/skills"], detect: [".ona"] },
206
+ { id: "posit-assistant", name: "Posit Assistant", short: "Pa", icon: "\xB7", color: "#8A93A6", project: [".posit/assistant/skills"], global: [".posit/assistant/skills"], detect: [".posit"] },
207
+ { id: "reasonix", name: "Reasonix", short: "Rx", icon: "\xB7", color: "#8A93A6", project: [".reasonix/skills"], global: [".reasonix/skills"], detect: [".reasonix"] },
208
+ { id: "sarvam-code", name: "Sarvam Code", short: "Sv", icon: "\xB7", color: "#8A93A6", project: [".agents/skills"], global: [".agents/skills"], detect: [".sarvam"] },
209
+ { id: "terramind", name: "Terramind", short: "Tm", icon: "\xB7", color: "#8A93A6", project: [".terramind/skills"], global: [".terramind/skills"], detect: [".terramind"] },
210
+ { id: "tinycloud", name: "TinyCloud", short: "Ty", icon: "\xB7", color: "#8A93A6", project: [".tinycloud/skills"], global: [".tinycloud/skills"], detect: [".tinycloud"] }
211
+ ];
212
+ var FEATURED_HARNESSES = ["claude-code", "cursor", "codex", "zed", "grok"];
142
213
  var home = userHome;
143
214
  var HARNESSES = [
144
215
  {
@@ -196,7 +267,19 @@ var HARNESSES = [
196
267
  // The first folder is where global links for Grok go, so those links stay out of ~/.grok/skills.
197
268
  globalDirs: () => [join2(home(), ".agents", "skills"), join2(grokDir(), "skills"), join2(home(), ".claude", "skills"), join2(home(), ".cursor", "skills")],
198
269
  installed: () => existsSync(grokDir()) || onPath("grok") || hasApp("Grok")
199
- }
270
+ },
271
+ ...EXTRAS.map(
272
+ (e) => ({
273
+ id: e.id,
274
+ name: e.name,
275
+ short: e.short,
276
+ icon: e.icon,
277
+ color: e.color,
278
+ projectDirs: [...e.project],
279
+ globalDirs: () => e.global.map((g) => join2(home(), g)),
280
+ installed: () => e.detect.some((d) => existsSync(join2(home(), d))) || ("bin" in e && e.bin ? onPath(e.bin) : false) || ("app" in e && e.app ? hasApp(e.app) : false)
281
+ })
282
+ )
200
283
  ];
201
284
  function onPath(bin) {
202
285
  const exts = process.platform === "win32" ? ["", ".exe", ".cmd", ".bat"] : [""];
@@ -215,20 +298,59 @@ function preference(dir) {
215
298
  const i = PREFERRED_DIRS.indexOf(dir);
216
299
  return i === -1 ? PREFERRED_DIRS.length : i;
217
300
  }
218
- function cost(dirs) {
219
- return dirs.length * 100 + dirs.reduce((sum, d) => sum + preference(d), 0);
301
+ function projectDirsInUse(enabled) {
302
+ return ALL_PROJECT_DIRS.filter((d) => d !== "skills" || enabled.includes("openclaw"));
303
+ }
304
+ function cover(enabled, universe, reads, pref) {
305
+ const left = new Set(enabled);
306
+ const chosen = [];
307
+ while (left.size) {
308
+ let best = null;
309
+ let bestCount = 0;
310
+ let bestPref = Infinity;
311
+ for (const dir of universe) {
312
+ if (chosen.includes(dir)) continue;
313
+ const count = [...left].filter((id) => reads(id).includes(dir)).length;
314
+ if (count === 0) continue;
315
+ const p = pref(dir);
316
+ if (count > bestCount || count === bestCount && (p < bestPref || p === bestPref && (best === null || dir < best))) {
317
+ best = dir;
318
+ bestCount = count;
319
+ bestPref = p;
320
+ }
321
+ }
322
+ if (!best) break;
323
+ chosen.push(best);
324
+ for (const id of [...left]) if (reads(id).includes(best)) left.delete(id);
325
+ }
326
+ return chosen;
220
327
  }
221
328
  function installDirs(enabled, keep) {
222
329
  if (enabled.length === 0) return [keep ?? PROJECT_SKILLS_DIR];
223
- let best = [];
224
- for (let mask = 1; mask < 1 << ALL_PROJECT_DIRS.length; mask++) {
225
- const dirs = ALL_PROJECT_DIRS.filter((_, i) => mask & 1 << i).sort((a, b) => preference(a) - preference(b));
226
- if (keep && !dirs.includes(keep)) continue;
227
- if (!enabled.every((id) => harness(id).projectDirs.some((d) => dirs.includes(d)))) continue;
228
- if (best.length === 0 || cost(dirs) < cost(best)) best = dirs;
229
- }
230
- const primary = keep ?? best[0];
231
- return [primary, ...best.filter((d) => d !== primary)];
330
+ const reads = (id) => harness(id).projectDirs;
331
+ const rest = keep ? enabled.filter((id) => !reads(id).includes(keep)) : enabled;
332
+ const chosen = [.../* @__PURE__ */ new Set([...keep ? [keep] : [], ...cover(rest, ALL_PROJECT_DIRS, reads, preference)])];
333
+ const primary = keep ?? [...chosen].sort((a, b) => preference(a) - preference(b) || a.localeCompare(b))[0];
334
+ return [primary, ...chosen.filter((d) => d !== primary).sort((a, b) => preference(a) - preference(b) || a.localeCompare(b))];
335
+ }
336
+ function globalPreference(dir) {
337
+ const agents = realPath(join2(userHome(), ".agents", "skills"));
338
+ const claude2 = realPath(join2(claudeDir(), "skills"));
339
+ const real = realPath(dir);
340
+ if (real === agents) return 0;
341
+ if (real === claude2) return 1;
342
+ return 2;
343
+ }
344
+ function globalInstallDirs(enabled) {
345
+ if (enabled.length === 0) return [];
346
+ const reads = (id) => harness(id).globalDirs();
347
+ const chosen = cover(enabled, [...new Set(enabled.flatMap(reads))], reads, globalPreference);
348
+ const claude2 = join2(claudeDir(), "skills");
349
+ const agents = join2(userHome(), ".agents", "skills");
350
+ const primary = chosen.find((d) => realPath(d) === realPath(claude2)) ?? chosen.find((d) => realPath(d) === realPath(agents)) ?? chosen[0];
351
+ if (!primary) return [];
352
+ const dirs = [primary, ...chosen.filter((d) => realPath(d) !== realPath(primary))];
353
+ return dirs.filter((d, i) => dirs.findIndex((o) => realPath(o) === realPath(d)) === i);
232
354
  }
233
355
 
234
356
  // src/project.ts
@@ -312,15 +434,25 @@ function indexFile(name) {
312
434
  return join4(storeDir(name), "versions.json");
313
435
  }
314
436
  function versionHistory(name) {
315
- return readState(indexFile(name)) ?? [];
437
+ const hit = historyCache.get(name);
438
+ if (hit) return hit;
439
+ const history = readState(indexFile(name)) ?? [];
440
+ historyCache.set(name, history);
441
+ return history;
316
442
  }
317
443
  function versionDir(name, version) {
318
444
  return join4(storeDir(name), String(version));
319
445
  }
320
446
  var latestCache = /* @__PURE__ */ new Map();
447
+ var historyCache = /* @__PURE__ */ new Map();
321
448
  function forgetLatest(name) {
322
- if (name) latestCache.delete(name);
323
- else latestCache.clear();
449
+ if (name) {
450
+ latestCache.delete(name);
451
+ historyCache.delete(name);
452
+ } else {
453
+ latestCache.clear();
454
+ historyCache.clear();
455
+ }
324
456
  }
325
457
  function latestVersion(name) {
326
458
  if (latestCache.has(name)) return latestCache.get(name) ?? null;
@@ -337,7 +469,9 @@ function latestVersion(name) {
337
469
  if (existsSync3(versionDir(name, next.version))) throw new Error(`${versionDir(name, next.version)} exists but ${indexFile(name)} doesn't list it: fix the index before adding versions`);
338
470
  mkdirSync2(storeDir(name), { recursive: true });
339
471
  copySkill(source, versionDir(name, next.version));
340
- writeState(indexFile(name), [...history, next]);
472
+ const recorded = [...history, next];
473
+ writeState(indexFile(name), recorded);
474
+ historyCache.set(name, recorded);
341
475
  result = next;
342
476
  }
343
477
  }
@@ -463,26 +597,63 @@ function normalizeRemote(url) {
463
597
  }
464
598
  var repos = /* @__PURE__ */ new Map();
465
599
  var changedAt = (path) => statSync3(path, { throwIfNoEntry: false })?.mtimeMs ?? -1;
600
+ function gitCommon(root) {
601
+ const gitPath = join5(root, ".git");
602
+ const st = statSync3(gitPath, { throwIfNoEntry: false });
603
+ if (!st) return null;
604
+ if (st.isDirectory()) return existsSync4(join5(gitPath, "HEAD")) ? gitPath : null;
605
+ if (!st.isFile()) return null;
606
+ const gitdir = readFileSync3(gitPath, "utf-8").match(/^gitdir:\s*(.+?)\s*$/m)?.[1];
607
+ if (!gitdir) return null;
608
+ const dir = resolve3(root, gitdir);
609
+ if (!existsSync4(dir)) return null;
610
+ let common = dir;
611
+ try {
612
+ const resolved = resolve3(dir, readFileSync3(join5(dir, "commondir"), "utf-8").trim());
613
+ if (existsSync4(resolved)) common = resolved;
614
+ } catch {
615
+ }
616
+ return existsSync4(join5(common, "HEAD")) ? common : null;
617
+ }
618
+ function remoteUrls(configPath) {
619
+ let text;
620
+ try {
621
+ text = readFileSync3(configPath, "utf-8");
622
+ } catch {
623
+ return [];
624
+ }
625
+ if (/^\s*\[include/m.test(text)) return null;
626
+ const urls = [];
627
+ let section = "";
628
+ for (const line of text.split(/\r?\n/)) {
629
+ const header = line.match(/^\s*\[([^\]]+)\]\s*$/);
630
+ if (header) {
631
+ const remote = header[1].match(/^remote\s+"(.+)"$/) ?? header[1].match(/^remote\s+(\S+)$/);
632
+ section = remote?.[1] ?? "";
633
+ continue;
634
+ }
635
+ const url = section ? line.match(/^\s*url\s*=\s*(.*?)\s*$/) : null;
636
+ if (url) urls.push([`remote.${section}.url`, url[1]]);
637
+ }
638
+ return urls;
639
+ }
466
640
  function repoOf(root) {
467
641
  const real = realPath(root);
468
642
  const hit = repos.get(real);
469
643
  if (hit && (hit.config ? changedAt(hit.config) === hit.seen : !existsSync4(join5(root, ".git")))) return hit.repo;
470
- const out2 = gitIn(root, ["rev-parse", "--show-prefix", "--git-common-dir"])?.split("\n");
471
- if (!out2) {
644
+ const commonPath = gitCommon(root);
645
+ if (!commonPath) {
472
646
  repos.set(real, { repo: null, config: null, seen: 0 });
473
647
  return null;
474
648
  }
475
- const common = realNative(resolve3(root, out2[1].trim())) ?? resolve3(root, out2[1].trim());
649
+ const common = realNative(commonPath) ?? commonPath;
476
650
  const folder = basename2(common) === ".git" ? dirname2(common) : common;
477
651
  const config = join5(common, "config");
478
652
  const seen = changedAt(config);
479
- let repo = null;
480
- if (out2[0] === "") {
481
- const urls = (gitIn(root, ["config", "--get-regexp", "^remote\\..*\\.url$"]) ?? "").split("\n").filter(Boolean).map((l) => [l.slice(0, l.indexOf(" ")), l.slice(l.indexOf(" ") + 1)]);
482
- const url = (urls.find(([k]) => k === "remote.origin.url") ?? urls[0])?.[1];
483
- const remote = url ? normalizeRemote(url) : void 0;
484
- repo = { file: pinsFileFor(root, remote, folder), folder, common, ...remote ? { remote } : {}, root };
485
- }
653
+ const urls = remoteUrls(config) ?? (gitIn(root, ["config", "--get-regexp", "^remote\\..*\\.url$"]) ?? "").split("\n").filter(Boolean).map((l) => [l.slice(0, l.indexOf(" ")), l.slice(l.indexOf(" ") + 1)]);
654
+ const url = (urls.find(([k]) => k === "remote.origin.url") ?? urls[0])?.[1];
655
+ const remote = url ? normalizeRemote(url) : void 0;
656
+ const repo = { file: pinsFileFor(root, remote, folder), folder, common, ...remote ? { remote } : {}, root };
486
657
  repos.set(real, { repo, config, seen });
487
658
  return repo;
488
659
  }
@@ -574,7 +745,10 @@ function isProjectCandidate(root) {
574
745
  function rememberProjects(roots) {
575
746
  const incoming = roots.map((r) => resolve3(r)).filter((r) => isProjectCandidate(r) && !isAgentWorktree(r));
576
747
  const all = onePerFolder([...knownProjects(), ...incoming]).sort();
577
- writeState(projectsFile(), all);
748
+ const path = projectsFile();
749
+ const prev = readState(path);
750
+ if (Array.isArray(prev) && prev.length === all.length && prev.every((p, i) => p === all[i])) return;
751
+ writeState(path, all);
578
752
  }
579
753
  function readableRoots(roots) {
580
754
  const unreadable = [];
@@ -639,7 +813,7 @@ function findProjects(dir, depth = 4) {
639
813
  if (depth < 0 || !existsSync5(dir)) return [];
640
814
  if (isProjectCandidate(dir)) {
641
815
  const isRepo = existsSync5(join6(dir, ".git"));
642
- const hasSkills = ALL_PROJECT_DIRS.some((d) => skillDirsIn(join6(dir, d)).length > 0) || existsSync5(join6(dir, MANIFEST_FILE));
816
+ const hasSkills = projectDirsInUse(enabledHarnesses()).some((d) => skillDirsIn(join6(dir, d)).length > 0) || existsSync5(join6(dir, MANIFEST_FILE));
643
817
  if (isRepo || hasSkills) return [dir];
644
818
  }
645
819
  let entries = [];
@@ -1112,7 +1286,7 @@ function blindSaid(name, blind, label = basename4) {
1112
1286
  return `${who} only ${blind.length === 1 ? "sees" : "see"} the global copy: ${fixes.join("; ")}, or keep ${name} global (skilllib global keep)`;
1113
1287
  }
1114
1288
  function dependencyDirs(root, name, dep) {
1115
- const untracked = ALL_PROJECT_DIRS.find((d) => d !== GROK_SKILLS_DIR && existsSync7(join8(root, d, name, "SKILL.md")) && !isLink(join8(root, d, name)));
1289
+ const untracked = projectDirsInUse(enabledHarnesses()).find((d) => d !== GROK_SKILLS_DIR && existsSync7(join8(root, d, name, "SKILL.md")) && !isLink(join8(root, d, name)));
1116
1290
  const primary = dep ? dep.dir ?? PROJECT_SKILLS_DIR : untracked ?? installDirs(enabledHarnesses())[0];
1117
1291
  const wanted = installDirs(enabledHarnesses(), primary);
1118
1292
  const real = (d) => realpathOrNull(join8(root, d));
@@ -1127,7 +1301,9 @@ function trackBlocker(root, name) {
1127
1301
  return `${MANIFEST_FILE} can't be read`;
1128
1302
  }
1129
1303
  const stuck = firstStuck(root, dependencyDirs(root, name, recorded));
1130
- return stuck ? blockerSaid(root, stuck) : null;
1304
+ if (stuck) return blockerSaid(root, stuck);
1305
+ if (!recorded && !personalPinsFile(root) && gitRepoAbove(root)) return "only the top folder of a git repo can have personal pins";
1306
+ return null;
1131
1307
  }
1132
1308
  function pinsSkill(root, name) {
1133
1309
  try {
@@ -1177,7 +1353,7 @@ function projectStatus(root) {
1177
1353
  const lock = projectSkillsLock(root);
1178
1354
  const unmanaged = [];
1179
1355
  const byName = /* @__PURE__ */ new Map();
1180
- for (const dir of ALL_PROJECT_DIRS.filter((d) => writableDir(root, d))) {
1356
+ for (const dir of projectDirsInUse(enabled).filter((d) => writableDir(root, d))) {
1181
1357
  for (const path of skillDirsIn(join8(root, dir))) {
1182
1358
  const name = basename4(path);
1183
1359
  if (seen.has(name)) continue;
@@ -1476,10 +1652,20 @@ function addSkill(root, name, { force = false, version, allowTracked = false, pe
1476
1652
  ...moved ? { moved } : {}
1477
1653
  };
1478
1654
  }
1655
+ function gitRepoAbove(dir) {
1656
+ let at = resolve6(dir);
1657
+ while (true) {
1658
+ if (existsSync7(join8(at, ".git", "HEAD"))) return true;
1659
+ const parent = dirname3(at);
1660
+ if (parent === at) return false;
1661
+ at = parent;
1662
+ }
1663
+ }
1479
1664
  function pinsWhere(root, recorded, personal) {
1480
- const want = personal ?? (recorded ? !!recorded.personal : !existsSync7(join8(root, MANIFEST_FILE)));
1481
- if (!want || personalPinsFile(root)) return want;
1482
- return personal ? "only the top folder of a git repo can have personal pins" : false;
1665
+ const want = personal ?? (recorded ? !!recorded.personal : !!personalPinsFile(root));
1666
+ if (want && !personalPinsFile(root)) return "only the top folder of a git repo can have personal pins";
1667
+ if (personal === void 0 && !recorded && !personalPinsFile(root) && gitRepoAbove(root)) return "only the top folder of a git repo can have personal pins";
1668
+ return want;
1483
1669
  }
1484
1670
  var personalOf = (manifest) => Object.fromEntries(Object.entries(manifest.skills).filter(([, d]) => d.personal));
1485
1671
  function removeSkill(root, name, { force = false } = {}) {
@@ -1754,12 +1940,7 @@ function shippedSkill() {
1754
1940
  return readFileSync5(new URL("../skills/skilllib/SKILL.md", import.meta.url), "utf-8");
1755
1941
  }
1756
1942
  function agentSkillDirs(enabled = enabledHarnesses()) {
1757
- const claudeSkills = join9(claudeDir(), "skills");
1758
- const agentsSkills = join9(userHome(), ".agents", "skills");
1759
- const needsClaude = enabled.some((id) => harness(id).globalDirs().includes(claudeSkills));
1760
- const needsAgents = enabled.some((id) => !harness(id).globalDirs().includes(claudeSkills));
1761
- const dirs = [...needsClaude ? [claudeSkills] : [], ...needsAgents ? [agentsSkills] : []];
1762
- return dirs.filter((d, i) => dirs.findIndex((o) => realPath(o) === realPath(d)) === i).map((d) => join9(d, AGENT_SKILL_NAME));
1943
+ return globalInstallDirs(enabled).map((d) => join9(d, AGENT_SKILL_NAME));
1763
1944
  }
1764
1945
  function isOurs(dir) {
1765
1946
  return existsSync8(join9(dir, OWN_SKILL_MARKER));
@@ -1790,7 +1971,7 @@ function installAgentSkill(enabled = enabledHarnesses()) {
1790
1971
  return { ok: true, dirs: [primary, ...links] };
1791
1972
  }
1792
1973
  function removeAgentSkill() {
1793
- const all = agentSkillDirs(["claude-code", "codex"]);
1974
+ const all = [...new Set(HARNESSES.flatMap((h) => h.globalDirs().map((d) => join9(d, AGENT_SKILL_NAME))))];
1794
1975
  const removed = all.filter((d) => entryExists(d) && (isOurs(d) || isDangling(d)));
1795
1976
  for (const d of removed) {
1796
1977
  if (!entryExists(d)) continue;
@@ -1807,9 +1988,9 @@ function refreshAgentSkill() {
1807
1988
  }
1808
1989
 
1809
1990
  // src/commands.ts
1810
- import { existsSync as existsSync14, readFileSync as readFileSync7 } from "fs";
1991
+ import { existsSync as existsSync15, readFileSync as readFileSync7, rmSync as rmSync8 } from "fs";
1811
1992
  import { parseArgs as parseFlags } from "util";
1812
- import { basename as basename10, join as join15, resolve as resolve8, sep as sep9 } from "path";
1993
+ import { basename as basename11, join as join17, resolve as resolve9, sep as sep10 } from "path";
1813
1994
 
1814
1995
  // src/git.ts
1815
1996
  import { execFileSync as execFileSync3 } from "child_process";
@@ -1828,11 +2009,13 @@ function git(root, args, input) {
1828
2009
  }
1829
2010
  function gitInfo(root, extraDirs = []) {
1830
2011
  let inRepo;
1831
- try {
1832
- inRepo = execFileSync3("git", ["rev-parse", "--show-prefix"], { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1833
- } catch {
1834
- if (!existsSync9(join10(root, ".git"))) return null;
1835
- inRepo = "";
2012
+ if (existsSync9(join10(root, ".git"))) inRepo = "";
2013
+ else {
2014
+ try {
2015
+ inRepo = execFileSync3("git", ["rev-parse", "--show-prefix"], { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
2016
+ } catch {
2017
+ return null;
2018
+ }
1836
2019
  }
1837
2020
  const real = (p) => {
1838
2021
  try {
@@ -2209,11 +2392,28 @@ function namesIn(dirs) {
2209
2392
  }
2210
2393
  return [...names].sort();
2211
2394
  }
2212
- var PROJECT_TIDY_DIRS = ALL_PROJECT_DIRS.filter((d) => d !== GROK_SKILLS_DIR);
2213
- var repoTidyDirs = (root) => PROJECT_TIDY_DIRS.filter((d) => notWritable(root, d) === null);
2395
+ var CLASSIC_TIDY = [PROJECT_SKILLS_DIR, AGENTS_SKILLS_DIR, ".cursor/skills", ".codex/skills"];
2396
+ var tidyDirs = () => {
2397
+ const enabled = enabledHarnesses();
2398
+ const fromEnabled = enabled.flatMap((id) => harness(id).projectDirs);
2399
+ return [.../* @__PURE__ */ new Set([...CLASSIC_TIDY, ...fromEnabled])].filter((d) => d !== GROK_SKILLS_DIR && (d !== "skills" || enabled.includes("openclaw")));
2400
+ };
2401
+ var repoTidyDirs = (root) => tidyDirs().filter((d) => notWritable(root, d) === null);
2214
2402
  function globalTidyDirs() {
2215
2403
  const own = realPath(join12(grokDir(), "skills"));
2216
- return globalSkillDirs().filter((d) => realPath(d) !== own);
2404
+ const allowed = new Set(
2405
+ [
2406
+ join12(userHome(), ".agents", "skills"),
2407
+ join12(claudeDir(), "skills"),
2408
+ join12(userHome(), ".cursor", "skills"),
2409
+ join12(userHome(), ".codex", "skills"),
2410
+ ...enabledHarnesses().flatMap((id) => harness(id).globalDirs())
2411
+ ].map((d) => realPath(d))
2412
+ );
2413
+ return globalSkillDirs().filter((d) => {
2414
+ const real = realPath(d);
2415
+ return real !== own && allowed.has(real);
2416
+ });
2217
2417
  }
2218
2418
  function runsFrom(id, dirsWithCopy, readsOf) {
2219
2419
  const reads = readsOf(id).filter((d) => dirsWithCopy.includes(d));
@@ -2401,7 +2601,7 @@ function intoLibrary(path, force = false) {
2401
2601
  return r.name;
2402
2602
  }
2403
2603
  var pinsOf = (where, personal) => personal ? "your personal pins" : `${where}'s skilllib.json`;
2404
- var newPinsOf = (root, where) => pinsOf(where, !existsSync12(join13(root, MANIFEST_FILE)) && personalPinsFile(root) !== null);
2604
+ var newPinsOf = (root, where) => pinsOf(where, personalPinsFile(root) !== null);
2405
2605
  function track(root, name) {
2406
2606
  const c = addSkill(root, name);
2407
2607
  if (c.action === "skipped") throw new Error(`${name}: ${c.reason}`);
@@ -2464,8 +2664,8 @@ function applyAll(plans, git2) {
2464
2664
  }
2465
2665
  function brokenLinksIn(root, opened) {
2466
2666
  const where = basename7(root);
2467
- const tidyDirs = repoTidyDirs(root);
2468
- const hasCopy = (name) => tidyDirs.some((d) => !isLink(join13(root, d, name)) && existsSync12(join13(root, d, name, "SKILL.md")));
2667
+ const tidyDirs2 = repoTidyDirs(root);
2668
+ const hasCopy = (name) => tidyDirs2.some((d) => !isLink(join13(root, d, name)) && existsSync12(join13(root, d, name, "SKILL.md")));
2469
2669
  const mends = (project, dir, name) => {
2470
2670
  const dep = pinsIn(project)[name];
2471
2671
  return !!dep && (dep.links ?? []).includes(dir) && !syncBlocker(project, name, dep) && versionForHash(name, dep.hash) !== null;
@@ -2485,7 +2685,7 @@ function brokenLinksIn(root, opened) {
2485
2685
  const seen = /* @__PURE__ */ new Set();
2486
2686
  const aliased = (dir) => (realNative(join13(root, dir)) ?? "") !== join13(realRoot, dir);
2487
2687
  const packageFolders = packageSkillFolders(root).map((f) => ({ dir: f.location, top: false })).sort((a, b) => Number(aliased(a.dir)) - Number(aliased(b.dir)));
2488
- const folders2 = [...ALL_PROJECT_DIRS.map((dir) => ({ dir, top: true })), ...packageFolders].flatMap(({ dir, top }) => {
2688
+ const folders2 = [...projectDirsInUse(enabledHarnesses()).map((dir) => ({ dir, top: true })), ...packageFolders].flatMap(({ dir, top }) => {
2489
2689
  const real = realNative(join13(root, dir)) ?? join13(root, dir);
2490
2690
  if (seen.has(real)) return [];
2491
2691
  seen.add(real);
@@ -2867,7 +3067,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
2867
3067
  }
2868
3068
  const agentSkill2 = agentSkillState();
2869
3069
  if (agentSkill2 !== "installed") {
2870
- const install = {
3070
+ const install2 = {
2871
3071
  label: agentSkill2 === "missing" ? "Install the skilllib skill for your agents" : "Update the skilllib skill",
2872
3072
  hint: "one global skill, in ~/.claude/skills (and ~/.agents/skills)",
2873
3073
  run: () => {
@@ -2882,7 +3082,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
2882
3082
  title: agentSkill2 === "missing" ? "Your agents don't know about skilllib" : "The skilllib skill for your agents is out of date",
2883
3083
  detail: "The skilllib skill lets you ask your agents which skills they can use here, where each comes from, and which of your skills a repo should add.",
2884
3084
  at: rows(null, ["skilllib"]),
2885
- ...agentSkill2 === "missing" ? { choices: [install] } : { fix: install }
3085
+ ...agentSkill2 === "missing" ? { choices: [install2] } : { fix: install2 }
2886
3086
  });
2887
3087
  }
2888
3088
  return issues;
@@ -2959,7 +3159,7 @@ function repoIssues(root, ctx) {
2959
3159
  if (drifted.length) {
2960
3160
  const seen = drifted.some((s) => isShared(gitOf(root)?.of(relativeTo(root, s.path))));
2961
3161
  const yours = drifted.some((s) => untrackable(s));
2962
- const install = {
3162
+ const install2 = {
2963
3163
  label: "Install the pinned versions",
2964
3164
  hint: seen ? "git will see the copies change" : "your library keeps every version",
2965
3165
  run: () => summarize(where, "synced", syncProject(root, { names: drifted.map((s) => s.name) }))
@@ -2970,7 +3170,7 @@ function repoIssues(root, ctx) {
2970
3170
  title: `${where}: ${plural(drifted.length, "skill")} not at the pinned version`,
2971
3171
  detail: `${drifted.map((s) => s.name).join(", ")}: each copy is another version from your library. sync installs the pinned one.${yours ? ` ${[...new Set(drifted.flatMap((s) => untrackable(s) ? [untrackable(s)] : []))].map((why) => `${drifted.filter((s) => untrackable(s) === why).map((s) => s.name).join(", ")}: ${why}`).join("; ")}, so skilllib can't pin the version that's there; ${drifted.filter((s) => untrackable(s)).length === 1 ? "it may be edits you copied into Your skills on purpose, and stays" : "they may be edits you copied into Your skills on purpose, and stay"} unless you choose this.` : ""}`,
2972
3172
  at: rows(root, drifted.map((s) => s.name)),
2973
- ...seen || yours ? { choices: [install] } : { fix: install }
3173
+ ...seen || yours ? { choices: [install2] } : { fix: install2 }
2974
3174
  });
2975
3175
  }
2976
3176
  for (const s of unstuck.filter((s2) => s2.state === "folder missing" && s2.latest === null))
@@ -3144,13 +3344,13 @@ function repoIssues(root, ctx) {
3144
3344
  id: `local:${root}:${s.name}`,
3145
3345
  severity: "suggestion",
3146
3346
  title: `${s.name} in ${where} isn't in your library`,
3147
- detail: "Import it into the library to reuse it in other projects and keep it backed up. The repo then tracks it (in skilllib.json, or your personal pins when it has none).",
3347
+ detail: `Import it into the library to reuse it in other projects and keep it backed up. The repo then tracks it in ${newPinsOf(root, where)}.`,
3148
3348
  at: rows(root, [s.name]),
3149
3349
  choices: [
3150
3350
  {
3151
3351
  label: "Import into the library",
3152
3352
  // Tracking it writes skilllib.json.
3153
- hint: `copies it into Your skills${gitNote(root, [join13(root, MANIFEST_FILE)])}`,
3353
+ hint: `copies it into Your skills${personalPinsFile(root) ? "" : gitNote(root, [join13(root, MANIFEST_FILE)])}`,
3154
3354
  run: () => {
3155
3355
  intoLibrary(s.path);
3156
3356
  track(root, s.name);
@@ -3207,7 +3407,7 @@ function repoIssues(root, ctx) {
3207
3407
  id: `npx:${root}:${s.name}`,
3208
3408
  severity: "suggestion",
3209
3409
  title: `${s.name} in ${where} came from npx skills, not your library`,
3210
- detail: `Installed with \`npx skills add\`${s.source ? ` (${s.source})` : ""}. Import it into your library to reuse it in other projects and keep it backed up${teams ? `; ${where}'s copy stays as it is (${committed(s) ? "committed" : `${untrackable(s)}, so skilllib can't track it here`})` : `; ${where} then tracks it (in skilllib.json, or your personal pins when it has none)`}.`,
3410
+ detail: `Installed with \`npx skills add\`${s.source ? ` (${s.source})` : ""}. Import it into your library to reuse it in other projects and keep it backed up${teams ? `; ${where}'s copy stays as it is (${committed(s) ? "committed" : `${untrackable(s)}, so skilllib can't track it here`})` : `; ${where} then tracks it in ${newPinsOf(root, where)}`}.`,
3211
3411
  at: rows(root, [s.name]),
3212
3412
  choices: [
3213
3413
  {
@@ -3478,8 +3678,8 @@ function libraryFor(currentProject) {
3478
3678
  }
3479
3679
 
3480
3680
  // src/usage.ts
3481
- import { createReadStream, existsSync as existsSync13, readdirSync as readdirSync10, statSync as statSync6 } from "fs";
3482
- import { join as join14 } from "path";
3681
+ import { createReadStream, existsSync as existsSync13, mkdirSync as mkdirSync9, readdirSync as readdirSync10, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync6, writeFileSync as writeFileSync6 } from "fs";
3682
+ import { dirname as dirname7, join as join14 } from "path";
3483
3683
  import { createInterface } from "readline";
3484
3684
  var BUILTIN_COMMANDS = /* @__PURE__ */ new Set([
3485
3685
  "add-dir",
@@ -3581,21 +3781,58 @@ function transcriptFiles(sinceMs) {
3581
3781
  });
3582
3782
  });
3583
3783
  }
3784
+ function usageCacheFile() {
3785
+ return join14(skilllibHome(), "usage-cache.json");
3786
+ }
3787
+ function readUsageCache() {
3788
+ const data = readJson(usageCacheFile());
3789
+ return data?.files && typeof data.files === "object" ? data : { files: {} };
3790
+ }
3791
+ function writeUsageCache(cache) {
3792
+ const path = usageCacheFile();
3793
+ const temp = `${path}.${process.pid}.tmp`;
3794
+ try {
3795
+ mkdirSync9(dirname7(path), { recursive: true });
3796
+ writeFileSync6(temp, JSON.stringify(cache));
3797
+ renameSync4(temp, path);
3798
+ } catch {
3799
+ rmSync6(temp, { force: true });
3800
+ }
3801
+ }
3802
+ async function usesIn(file) {
3803
+ const uses = [];
3804
+ const lines = createInterface({ input: createReadStream(file), crlfDelay: Infinity });
3805
+ for await (const line of lines) uses.push(...usesFromTranscriptLine(line));
3806
+ return uses;
3807
+ }
3584
3808
  async function scanUsage(days) {
3585
3809
  const sinceMs = Date.now() - days * 24 * 60 * 60 * 1e3;
3810
+ const files = transcriptFiles(sinceMs);
3811
+ const cache = readUsageCache();
3812
+ let dirty = false;
3813
+ for (const file of Object.keys(cache.files)) {
3814
+ if (!files.includes(file) && !existsSync13(file)) {
3815
+ delete cache.files[file];
3816
+ dirty = true;
3817
+ }
3818
+ }
3586
3819
  const seen = /* @__PURE__ */ new Map();
3587
- for (const file of transcriptFiles(sinceMs)) {
3820
+ for (const file of files) {
3588
3821
  try {
3589
- const lines = createInterface({ input: createReadStream(file), crlfDelay: Infinity });
3590
- for await (const line of lines) {
3591
- for (const use of usesFromTranscriptLine(line)) {
3592
- if (Date.parse(use.at) >= sinceMs) seen.set(use.key, use);
3593
- }
3822
+ const st = statSync6(file);
3823
+ const mtimeMs = Math.round(st.mtimeMs);
3824
+ const hit = cache.files[file];
3825
+ const uses = hit && hit.size === st.size && hit.mtimeMs === mtimeMs && Array.isArray(hit.uses) ? hit.uses : await usesIn(file);
3826
+ if (uses !== hit?.uses) {
3827
+ cache.files[file] = { size: st.size, mtimeMs, uses };
3828
+ dirty = true;
3594
3829
  }
3830
+ for (const use of uses) if (Date.parse(use.at) >= sinceMs) seen.set(use.key, use);
3595
3831
  } catch {
3596
3832
  continue;
3597
3833
  }
3598
3834
  }
3835
+ if (dirty) writeUsageCache(cache);
3599
3836
  return [...seen.values()];
3600
3837
  }
3601
3838
  var skillOf = (key) => key.split(":").pop();
@@ -3625,12 +3862,298 @@ function usesByProject(uses, projectOf) {
3625
3862
  return byProject;
3626
3863
  }
3627
3864
 
3865
+ // src/install.ts
3866
+ import { execFileSync as execFileSync5 } from "child_process";
3867
+ import { createWriteStream, existsSync as existsSync14, lstatSync as lstatSync5, mkdirSync as mkdirSync10, mkdtempSync, readdirSync as readdirSync11, rmSync as rmSync7, statSync as statSync7 } from "fs";
3868
+ import { tmpdir as tmpdir2 } from "os";
3869
+ import { basename as basename9, join as join15, resolve as resolve8, sep as sep9 } from "path";
3870
+ import { Readable } from "stream";
3871
+ import { pipeline } from "stream/promises";
3872
+ var SEARCH_API = process.env.SKILLLIB_SKILLS_API || "https://skills.sh";
3873
+ var MAX_BYTES = 32 * 1024 * 1024;
3874
+ var SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "coverage"]);
3875
+ async function searchSkills(query) {
3876
+ const url = `${SEARCH_API}/api/search?${new URLSearchParams({ q: query, limit: "20" })}`;
3877
+ const res = await fetch(url);
3878
+ if (!res.ok) throw new Error(`skills.sh search failed (${res.status})`);
3879
+ const data = await res.json();
3880
+ return (data.skills ?? []).map((s) => ({
3881
+ name: s.name || s.skillId || s.id || "",
3882
+ source: s.source || "",
3883
+ skillId: s.skillId || s.name || "",
3884
+ installs: s.installs ?? 0
3885
+ }));
3886
+ }
3887
+ var NAME = /^[A-Za-z0-9_.-]+$/;
3888
+ function parseInstallSpec(input) {
3889
+ const trimmed = input.trim().replace(/\/+$/, "");
3890
+ if (!trimmed) return null;
3891
+ if (trimmed.startsWith(".") || trimmed.startsWith("/") || trimmed.startsWith("~") || /^[A-Za-z]:[\\/]/.test(trimmed)) {
3892
+ return { local: trimmed.replace(/^~(?=$|[\\/])/, userHome()) };
3893
+ }
3894
+ let text = trimmed;
3895
+ try {
3896
+ const url = new URL(trimmed);
3897
+ const host = url.hostname.replace(/^www\./, "");
3898
+ const parts2 = url.pathname.split("/").filter(Boolean);
3899
+ if (host === "skills.sh" && parts2.length >= 2) text = parts2.length >= 3 ? `${parts2[0]}/${parts2[1]}@${parts2.slice(2).join("/")}` : `${parts2[0]}/${parts2[1]}`;
3900
+ else if (host === "github.com" && parts2.length >= 2) {
3901
+ const repo = parts2[1].replace(/\.git$/, "");
3902
+ if (parts2[2] === "tree" && parts2.length >= 5) return namePair(parts2[0], repo, parts2.slice(4).join("/"));
3903
+ return namePair(parts2[0], repo);
3904
+ } else return null;
3905
+ } catch {
3906
+ }
3907
+ const at = text.split("@");
3908
+ if (at.length === 2 && at[0]?.includes("/") && at[1] && at[0].split("/").length === 2) {
3909
+ const [owner, repo] = at[0].split("/");
3910
+ return namePair(owner, repo, at[1]);
3911
+ }
3912
+ const parts = text.split("/");
3913
+ if (parts.length === 2) return namePair(parts[0], parts[1]);
3914
+ if (parts.length >= 3) return namePair(parts[0], parts[1], parts.slice(2).join("/"));
3915
+ return null;
3916
+ }
3917
+ function namePair(owner, repo, skill) {
3918
+ if (!NAME.test(owner) || !NAME.test(repo) || owner === "." || repo === "." || owner === ".." || repo === "..") return null;
3919
+ return skill ? { owner, repo, skill } : { owner, repo };
3920
+ }
3921
+ function escapes(root, path) {
3922
+ const base = realpathOrNull(root);
3923
+ const real = realpathOrNull(path);
3924
+ if (!base || !real) return true;
3925
+ return real !== base && !real.startsWith(base + sep9);
3926
+ }
3927
+ function findSkillDirs(root, depth = 0) {
3928
+ if (depth > 6 || !isDir(root)) return [];
3929
+ if (isSkillDir(root)) return escapes(root, root) ? [] : [root];
3930
+ let names = [];
3931
+ try {
3932
+ names = readdirSync11(root);
3933
+ } catch {
3934
+ return [];
3935
+ }
3936
+ return names.filter((n) => !n.startsWith(".") && !SKIP.has(n)).flatMap((n) => {
3937
+ const path = join15(root, n);
3938
+ const st = lstatSync5(path, { throwIfNoEntry: false });
3939
+ if (!st?.isDirectory()) return [];
3940
+ if (st.isSymbolicLink() && escapes(root, path)) return [];
3941
+ return findSkillDirs(path, depth + 1);
3942
+ });
3943
+ }
3944
+ function skillNamed(dirs, skill) {
3945
+ if (!skill) return dirs;
3946
+ const want = skill.split("/").filter(Boolean).pop();
3947
+ return dirs.filter((d) => basename9(d) === want || readSkillInfo(d).name === want);
3948
+ }
3949
+ async function downloadGitHub(owner, repo) {
3950
+ const dest = mkdtempSync(join15(tmpdir2(), "skilllib-"));
3951
+ const archive = join15(dest, "repo.tar.gz");
3952
+ const url = `https://codeload.github.com/${owner}/${repo}/tar.gz/HEAD`;
3953
+ let res;
3954
+ try {
3955
+ res = await fetch(url, { redirect: "follow" });
3956
+ } catch (err) {
3957
+ rmSync7(dest, { recursive: true, force: true });
3958
+ throw new Error(`couldn't download ${owner}/${repo} (${err instanceof Error ? err.message : err})`);
3959
+ }
3960
+ if (!res.ok || !res.body) {
3961
+ rmSync7(dest, { recursive: true, force: true });
3962
+ throw new Error(res.status === 404 ? `${owner}/${repo} not found on GitHub` : `couldn't download ${owner}/${repo} (${res.status})`);
3963
+ }
3964
+ const host = new URL(res.url).hostname;
3965
+ if (host !== "codeload.github.com" && host !== "github.com") {
3966
+ rmSync7(dest, { recursive: true, force: true });
3967
+ throw new Error(`refusing to download ${owner}/${repo} from ${host}`);
3968
+ }
3969
+ let bytes = 0;
3970
+ try {
3971
+ await pipeline(
3972
+ Readable.from(
3973
+ (async function* () {
3974
+ const reader = res.body.getReader();
3975
+ for (; ; ) {
3976
+ const { done, value } = await reader.read();
3977
+ if (done) return;
3978
+ bytes += value.byteLength;
3979
+ if (bytes > MAX_BYTES) throw new Error(`${owner}/${repo} is larger than ${MAX_BYTES / 1024 / 1024}MB`);
3980
+ yield value;
3981
+ }
3982
+ })()
3983
+ ),
3984
+ createWriteStream(archive)
3985
+ );
3986
+ execFileSync5("tar", ["-xzf", archive, "-C", dest], { stdio: "ignore" });
3987
+ } catch (err) {
3988
+ rmSync7(dest, { recursive: true, force: true });
3989
+ if (err instanceof Error && /larger than|couldn't download/.test(err.message)) throw err;
3990
+ throw new Error(`couldn't extract ${owner}/${repo}`);
3991
+ }
3992
+ rmSync7(archive, { force: true });
3993
+ const top = readdirSync11(dest);
3994
+ const root = top.length === 1 && statSync7(join15(dest, top[0])).isDirectory() ? join15(dest, top[0]) : dest;
3995
+ return { tmp: dest, root };
3996
+ }
3997
+ function installGlobal(name, { force = false } = {}) {
3998
+ const latest = latestVersion(name);
3999
+ if (!latest) return { ok: false, reason: "not in the library" };
4000
+ const parents = globalInstallDirs(enabledHarnesses());
4001
+ if (!parents.length) return { ok: false, reason: "no agents chosen yet (skilllib harnesses)" };
4002
+ const [primary, ...links] = parents;
4003
+ const dest = join15(primary, name);
4004
+ if (entryExists(dest) && !isLink(dest) && !isDir(dest)) return { ok: false, reason: `${dest} is a file` };
4005
+ const there = treeHash(dest);
4006
+ if (there && there !== latest.hash && versionForHash(name, there) === null && !force) {
4007
+ return { ok: false, reason: `${name} is already global, with edits that aren't in your library (pass --force to replace them)` };
4008
+ }
4009
+ if (there !== latest.hash) {
4010
+ if (isLink(dest)) rmSync7(dest, { force: true });
4011
+ mkdirSync10(primary, { recursive: true });
4012
+ copySkill(versionDir(name, latest.version), dest);
4013
+ }
4014
+ const wrote = [dest];
4015
+ for (const dir of links) {
4016
+ const link2 = join15(dir, name);
4017
+ if (isLink(link2) && realpathOrNull(link2) === null) rmSync7(link2, { force: true });
4018
+ else if (entryExists(link2)) {
4019
+ if (realpathOrNull(link2) === realpathOrNull(dest)) wrote.push(link2);
4020
+ continue;
4021
+ }
4022
+ mkdirSync10(dir, { recursive: true });
4023
+ linkDir(realpathOrNull(dest) ?? dest, join15(realpathOrNull(dir) ?? dir, name));
4024
+ wrote.push(link2);
4025
+ }
4026
+ setKeepGlobal([name], true);
4027
+ return { ok: true, dirs: wrote };
4028
+ }
4029
+ function installFound(dirs, source, { force = false, global = false, allowTracked = false } = {}) {
4030
+ const root = global ? null : projectHere();
4031
+ const label = source.startsWith("/") || source.startsWith(".") ? source : `skills.sh: ${source}`;
4032
+ return dirs.map((dir) => {
4033
+ const imported = importSkill(dir, { force });
4034
+ if (imported.status !== "exists") recordOrigin(imported.name, label);
4035
+ const row = { name: imported.name, source, status: imported.status };
4036
+ if (imported.status === "exists") {
4037
+ row.reason = "the library already has a different version (pass --force)";
4038
+ return row;
4039
+ }
4040
+ if (global) {
4041
+ const placed = installGlobal(imported.name, { force });
4042
+ if (!placed.ok) {
4043
+ row.status = "skipped";
4044
+ row.reason = placed.reason;
4045
+ } else row.global = placed.dirs;
4046
+ return row;
4047
+ }
4048
+ if (!root) return row;
4049
+ row.project = addSkill(root, imported.name, { force, allowTracked });
4050
+ if (row.project.action === "skipped") {
4051
+ row.status = "skipped";
4052
+ row.reason = row.project.reason;
4053
+ }
4054
+ return row;
4055
+ });
4056
+ }
4057
+ async function resolveInstallDir(spec) {
4058
+ if ("local" in spec) {
4059
+ const path = resolve8(spec.local);
4060
+ if (!existsSync14(path)) throw new Error(`${path} doesn't exist`);
4061
+ const dirs2 = isSkillDir(path) ? [path] : findSkillDirs(path);
4062
+ return { dirs: dirs2, source: path, many: dirs2.length > 1 };
4063
+ }
4064
+ const got = await downloadGitHub(spec.owner, spec.repo);
4065
+ const source = spec.skill ? `${spec.owner}/${spec.repo}@${spec.skill}` : `${spec.owner}/${spec.repo}`;
4066
+ const dirs = skillNamed(findSkillDirs(got.root), spec.skill);
4067
+ return { tmp: got.tmp, dirs, source, many: !spec.skill && dirs.length > 1 };
4068
+ }
4069
+
4070
+ // src/check.ts
4071
+ import { join as join16 } from "path";
4072
+ function promptTokens(name, description) {
4073
+ return Math.max(1, Math.ceil((name.length + 1 + description.length) / 4));
4074
+ }
4075
+ function freshLabel(state, version, latest) {
4076
+ if (state === "update available") return `v${version} \u2192 v${latest}`;
4077
+ if (state === "edited locally, update available") return `edited here, library has v${latest}`;
4078
+ if (state === "edited locally") return "edited here";
4079
+ if (state === "out of sync") return "not the pinned version";
4080
+ if (state === "folder missing") return "folder missing";
4081
+ if (state === "ok" || state === "repo skill, in library" || state === "untracked copy of library skill") return "current";
4082
+ if (state === "untracked, differs from library" || state === "repo skill, differs from library") return "differs from your library";
4083
+ return "not in your library";
4084
+ }
4085
+ function libraryFresh(path, name) {
4086
+ const lib = join16(libraryDir(), name);
4087
+ if (!isSkillDir(lib)) return "not in your library";
4088
+ const here = treeHash(path);
4089
+ return here && here === treeHash(lib) ? "matches your library" : "differs from your library";
4090
+ }
4091
+ var settled = /* @__PURE__ */ new Set(["current", "matches your library", "vendor", "skilllib"]);
4092
+ function byWeight(rows2) {
4093
+ const rank = (r) => (r.uses === 0 ? 0 : 1) * 2 + (settled.has(r.fresh) ? 1 : 0);
4094
+ return [...rows2].sort((a, b) => rank(a) - rank(b) || b.everyPrompt - a.everyPrompt || a.name.localeCompare(b.name));
4095
+ }
4096
+ function tokenSum(rows2, where) {
4097
+ return rows2.filter((r) => !where || r.where === where).reduce((n, r) => n + r.everyPrompt, 0);
4098
+ }
4099
+ function rough(n) {
4100
+ const rounded = n >= 100 ? Math.round(n / 100) * 100 : n;
4101
+ return String(rounded).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
4102
+ }
4103
+ function checkBrief(rows2, ctx) {
4104
+ const global = rows2.filter((r) => r.where === "global");
4105
+ const repo = rows2.filter((r) => r.where === "repo");
4106
+ const globalTokens = tokenSum(global);
4107
+ const unused = rows2.filter((r) => r.uses === 0);
4108
+ const unusedGlobal = global.filter((r) => r.uses === 0 && r.lever !== "skilllib");
4109
+ const unusedTokens = tokenSum(unusedGlobal);
4110
+ const n = (count) => `${count} skill${count === 1 ? "" : "s"}`;
4111
+ const lines = [
4112
+ `${n(rows2.length)}. ~${rough(globalTokens)} tokens of names and descriptions in every prompt${ctx.repo ? `, plus ~${rough(tokenSum(repo))} from this repo` : ""}.`,
4113
+ `${unused.length} had no Claude Code use in ${ctx.days} days. A zero is a hint: Cursor and Codex aren't in these transcripts.`
4114
+ ];
4115
+ if (!globalTokens) return [...lines, "Nothing global is loading."];
4116
+ lines.push(
4117
+ unusedTokens * 2 >= globalTokens ? "Heavy. More than half of every prompt's skill list had no use." : unusedTokens * 4 >= globalTokens ? "Mixed. A good share of every prompt is skills with no use." : "Fine. Most of what loads has been used."
4118
+ );
4119
+ const plugins = Object.entries(Object.groupBy(global.filter((r) => r.lever === "plugin"), (r) => r.via ?? r.name)).map(([id, skills = []]) => ({
4120
+ id,
4121
+ tokens: tokenSum(skills),
4122
+ idle: tokenSum(skills.filter((s) => s.uses === 0)),
4123
+ skills: skills.length,
4124
+ used: skills.filter((s) => s.uses > 0).length
4125
+ })).filter((p) => p.idle > 0 && p.idle * 2 >= p.tokens).sort((a, b) => b.tokens - a.tokens);
4126
+ const yours = unusedGlobal.filter((r) => r.lever === "yours");
4127
+ const synced = unusedGlobal.filter((r) => r.lever === "claude.ai");
4128
+ const drop = plugins.reduce((n2, p) => n2 + p.tokens, 0) + tokenSum(yours) + tokenSum(synced);
4129
+ if (drop > 0) lines.push(`You can drop ~${rough(drop)} tokens.`);
4130
+ for (const p of plugins.slice(0, 3)) {
4131
+ lines.push(
4132
+ p.used ? `~${rough(p.tokens)} is ${p.id} (${p.skills} skills, ${p.used} used). Turning the plugin off drops all of it, including ${p.used === 1 ? "that one" : "those"}.` : `~${rough(p.tokens)} is ${p.id} (${p.skills} skills, no use). Turn the plugin off.`
4133
+ );
4134
+ }
4135
+ if (plugins.length > 3) lines.push(`${plugins.length - 3} more plugins are in the same spot.`);
4136
+ if (plugins.length) lines.push("Turn a Claude Code plugin off with skilllib plugin off <id>. Cursor plugins turn off in Cursor.");
4137
+ if (yours.length) {
4138
+ const names = yours.sort((a, b) => b.everyPrompt - a.everyPrompt).slice(0, 5).map((r) => r.name);
4139
+ const more = yours.length > 5 ? `, and ${yours.length - 5} more` : "";
4140
+ lines.push(`~${rough(tokenSum(yours))} are your own global skills with no use (${names.join(", ")}${more}). Move them into the repos that need them, or delete them.`);
4141
+ }
4142
+ if (synced.length) lines.push(`~${rough(tokenSum(synced))} are claude.ai skills with no use. Turn them off on claude.ai.`);
4143
+ const builtins = unusedGlobal.filter((r) => r.lever === "built-in");
4144
+ if (builtins.length) {
4145
+ const who = [...new Set(builtins.map((r) => r.via ?? "the agent"))].join(", ");
4146
+ lines.push(`~${rough(tokenSum(builtins))} are ${who} built-ins with no use. They ship with the agent. skilllib can't unload them.`);
4147
+ }
4148
+ return lines;
4149
+ }
4150
+
3628
4151
  // src/move.ts
3629
- import { basename as basename9 } from "path";
4152
+ import { basename as basename10 } from "path";
3630
4153
  function moveGlobalSkills(skills, roots, {
3631
4154
  group,
3632
4155
  allowTracked = false,
3633
- label = basename9,
4156
+ label = basename10,
3634
4157
  allowTrackedHint = "re-run with --allow-tracked"
3635
4158
  } = {}) {
3636
4159
  const byName = /* @__PURE__ */ new Map();
@@ -3749,9 +4272,9 @@ function projectOfFactory(projects2) {
3749
4272
  const sorted2 = [...projects2].sort((a, b) => b.length - a.length);
3750
4273
  return (cwd) => {
3751
4274
  if (!cwd) return null;
3752
- const known = sorted2.find((p) => cwd === p || cwd.startsWith(p + sep9));
4275
+ const known = sorted2.find((p) => cwd === p || cwd.startsWith(p + sep10));
3753
4276
  if (known) return known;
3754
- const repo = cwd.split(`${sep9}.claude${sep9}worktrees${sep9}`)[0];
4277
+ const repo = cwd.split(`${sep10}.claude${sep10}worktrees${sep10}`)[0];
3755
4278
  return isProjectCandidate(repo) ? repo : null;
3756
4279
  };
3757
4280
  }
@@ -3765,7 +4288,7 @@ var projectsFor = (args) => {
3765
4288
  if (!args.all) return [requireProject()];
3766
4289
  const { readable, unreadable } = readableRoots(visibleProjects());
3767
4290
  for (const u of unreadable) {
3768
- error(`${basename10(u.root)}: ${u.reason}`);
4291
+ error(`${basename11(u.root)}: ${u.reason}`);
3769
4292
  process.exitCode = 1;
3770
4293
  }
3771
4294
  return readable;
@@ -3777,7 +4300,7 @@ function requireProject() {
3777
4300
  if (!isProjectCandidate(findProjectRoot())) {
3778
4301
  const top = findProjectRoot();
3779
4302
  if (realPath(top) !== realPath(userHome())) throw new Error(`${tildify(top)} is skilllib's or an agent's own folder, not a project. Run this inside a project.`);
3780
- const stray = existsSync14(join15(userHome(), MANIFEST_FILE)) ? `, though ${tildify(join15(userHome(), MANIFEST_FILE))} makes it look like one` : "";
4303
+ const stray = existsSync15(join17(userHome(), MANIFEST_FILE)) ? `, though ${tildify(join17(userHome(), MANIFEST_FILE))} makes it look like one` : "";
3781
4304
  throw new Error(`Your home folder isn't a project${stray}: skills added here would load in every repo. Run this inside a project.`);
3782
4305
  }
3783
4306
  throw new Error("Not in a git repo. Run this inside a project (or `git init` first).");
@@ -3799,7 +4322,7 @@ async function status(args) {
3799
4322
  const root = requireProject();
3800
4323
  const skills = projectStatus(root);
3801
4324
  const nested = nestedSkills(root);
3802
- info(`${basename10(root)} ${dim(tildify(root))}
4325
+ info(`${basename11(root)} ${dim(tildify(root))}
3803
4326
  `);
3804
4327
  if (skills.length === 0 && nested.length === 0) info("No skills in this project yet. Add one from your library with: skilllib add <name>");
3805
4328
  const usage2 = await usageBySkill(args.days, root);
@@ -3872,7 +4395,7 @@ async function list(args) {
3872
4395
  const installs = /* @__PURE__ */ new Map();
3873
4396
  for (const project of visibleProjects()) {
3874
4397
  for (const name of Object.keys(pinsIn(project))) {
3875
- installs.set(name, [...installs.get(name) ?? [], basename10(project)]);
4398
+ installs.set(name, [...installs.get(name) ?? [], basename11(project)]);
3876
4399
  }
3877
4400
  }
3878
4401
  const usage2 = await usageBySkill(args.days);
@@ -3915,7 +4438,7 @@ function teamStateIn(root, extraDirs = []) {
3915
4438
  function projects(args) {
3916
4439
  const { readable: roots, unreadable } = readableRoots(visibleProjects());
3917
4440
  for (const u of unreadable) {
3918
- error(`${basename10(u.root)}: ${u.reason}`);
4441
+ error(`${basename11(u.root)}: ${u.reason}`);
3919
4442
  process.exitCode = 1;
3920
4443
  }
3921
4444
  if (args.json) {
@@ -3924,7 +4447,7 @@ function projects(args) {
3924
4447
  roots.map((root) => {
3925
4448
  const teamState = teamStateIn(root);
3926
4449
  return {
3927
- name: basename10(root),
4450
+ name: basename11(root),
3928
4451
  path: root,
3929
4452
  skills: projectStatus(root).map((s) => ({ name: s.name, managed: s.managed, state: teamState(s), version: s.version, latest: s.latest }))
3930
4453
  };
@@ -3942,7 +4465,7 @@ function projects(args) {
3942
4465
  const usual = /* @__PURE__ */ new Set(["ok", "local only", "repo skill", "repo skill, in library", "from npx skills"]);
3943
4466
  const attention = skills.filter((s) => !usual.has(teamState(s))).length;
3944
4467
  return {
3945
- Project: basename10(root),
4468
+ Project: basename11(root),
3946
4469
  Path: dim(tildify(root)),
3947
4470
  Skills: skills.map((s) => s.name).join(", ") || dim("none"),
3948
4471
  "Local only": String(skills.filter((s) => teamState(s) === "local only").length),
@@ -3961,7 +4484,7 @@ function add(args) {
3961
4484
  const pins = readManifest(root).skills;
3962
4485
  const done = changes.filter((c) => c.action !== "skipped" && pins[c.name]);
3963
4486
  if (!args.json && args.personal === void 0 && done.some((c) => c.action === "installed" && !c.moved && pins[c.name].personal))
3964
- info(dim(`Pinned in your personal pins (${tildify(personalPinsDir())}) and kept out of git. --shared pins in skilllib.json for the team.`));
4487
+ info(dim(`Pinned for you (${tildify(personalPinsDir())}), kept out of git. To share it with the team: skilllib add <name> --shared, then commit skilllib.json and the skill folders.`));
3965
4488
  rememberProjects([root]);
3966
4489
  }
3967
4490
  function remove(args) {
@@ -3977,11 +4500,11 @@ function changeEach(args, change, nothing) {
3977
4500
  const changes = change(root);
3978
4501
  if (changes.some(incomplete)) process.exitCode = 1;
3979
4502
  if (!args.json) {
3980
- if (args.all) info(`${basename10(root)} ${dim(tildify(root))}`);
4503
+ if (args.all) info(`${basename11(root)} ${dim(tildify(root))}`);
3981
4504
  if (changes.length === 0) nothing(root);
3982
4505
  else printChanges(root, changes, args);
3983
4506
  }
3984
- return { project: basename10(root), path: root, changes: withDirs(root, changes) };
4507
+ return { project: basename11(root), path: root, changes: withDirs(root, changes) };
3985
4508
  });
3986
4509
  if (args.json) args.all ? json("changes --all", results) : json("changes", results[0].changes);
3987
4510
  return roots;
@@ -4012,7 +4535,7 @@ function update(args) {
4012
4535
  function outdated(args) {
4013
4536
  const roots = projectsFor(args);
4014
4537
  const behind = roots.flatMap(
4015
- (root) => projectStatus(root).filter((s) => s.state.endsWith("update available") && s.latest !== null && s.version !== null).map((s) => ({ project: basename10(root), path: root, skill: s.name, installed: s.version, latest: s.latest, state: s.state }))
4538
+ (root) => projectStatus(root).filter((s) => s.state.endsWith("update available") && s.latest !== null && s.version !== null).map((s) => ({ project: basename11(root), path: root, skill: s.name, installed: s.version, latest: s.latest, state: s.state }))
4016
4539
  );
4017
4540
  if (args.json) return json("outdated", behind);
4018
4541
  const rows2 = behind.map((b) => ({ Project: b.project, Skill: b.skill, Installed: `v${b.installed}`, Latest: `v${b.latest}`, Status: colorState(b.state) }));
@@ -4023,7 +4546,7 @@ function outdated(args) {
4023
4546
  }
4024
4547
  function importSkills(args) {
4025
4548
  if (args.global && args.positional.length) throw new Error("skilllib import --global takes no folders: it imports all your global skills");
4026
- const dirs = args.global ? yourGlobals().map((m) => m.path) : args.positional.map((p) => resolve8(p));
4549
+ const dirs = args.global ? yourGlobals().map((m) => m.path) : args.positional.map((p) => resolve9(p));
4027
4550
  if (dirs.length === 0) {
4028
4551
  throw new Error("Usage: skilllib import <skill-dir>... [--force] | skilllib import --global");
4029
4552
  }
@@ -4043,11 +4566,11 @@ function importSkills(args) {
4043
4566
  continue;
4044
4567
  }
4045
4568
  success(`${name} ${dim(result === "unchanged" ? "already in library" : result === "added" ? "added to library" : "library updated")}`);
4046
- const root = resolve8(dir, "..", "..", "..");
4569
+ const root = resolve9(dir, "..", "..", "..");
4047
4570
  const dep = projectHere(root) === root ? readManifest(root).skills[name] : void 0;
4048
- if (dep && realpathOrNull(join15(root, dep.dir ?? PROJECT_SKILLS_DIR, name)) === realpathOrNull(dir) && dep.hash !== treeHash(dir)) {
4571
+ if (dep && realpathOrNull(join17(root, dep.dir ?? PROJECT_SKILLS_DIR, name)) === realpathOrNull(dir) && dep.hash !== treeHash(dir)) {
4049
4572
  const change = addSkill(root, name);
4050
- if (change.action !== "skipped") info(dim(` ${basename10(root)} now depends on ${name} v${change.to}`));
4573
+ if (change.action !== "skipped") info(dim(` ${basename11(root)} now depends on ${name} v${change.to}`));
4051
4574
  }
4052
4575
  }
4053
4576
  if (args.json) json("import", results);
@@ -4063,14 +4586,14 @@ function scan(args) {
4063
4586
  const library = new Set(librarySkills().map((s) => s.name));
4064
4587
  const skillNames = (root) => [
4065
4588
  // Not a folder that links out of the repo: what's in it isn't the repo's (as in status).
4066
- .../* @__PURE__ */ new Set([...ALL_PROJECT_DIRS.filter((d) => !notWritable(root, d)).flatMap((d) => skillDirsIn(join15(root, d))).map((d) => basename10(d)), ...nestedSkills(root).map((s) => s.name)])
4589
+ .../* @__PURE__ */ new Set([...projectDirsInUse(enabledHarnesses()).filter((d) => !notWritable(root, d)).flatMap((d) => skillDirsIn(join17(root, d))).map((d) => basename11(d)), ...nestedSkills(root).map((s) => s.name)])
4067
4590
  ];
4068
4591
  const withSkills = found.filter((root) => skillNames(root).length > 0);
4069
4592
  info(`Scanned ${roots.map((r) => tildify(r)).join(", ")}: ${found.length} project${found.length === 1 ? "" : "s"}, ${withSkills.length} with skills
4070
4593
  `);
4071
4594
  table(
4072
4595
  withSkills.map((root) => ({
4073
- Project: basename10(root),
4596
+ Project: basename11(root),
4074
4597
  Path: dim(tildify(root)),
4075
4598
  Skills: skillNames(root).map((n) => library.has(n) ? n : yellow(n)).join(", ")
4076
4599
  }))
@@ -4127,7 +4650,7 @@ function newSkill(args) {
4127
4650
  if (!r.ok) {
4128
4651
  throw new Error(`Can't create ${name}: ${r.reason}`);
4129
4652
  }
4130
- success(`Created ${tildify(join15(r.dir, "SKILL.md"))}`);
4653
+ success(`Created ${tildify(join17(r.dir, "SKILL.md"))}`);
4131
4654
  }
4132
4655
  function restore(args) {
4133
4656
  const backups = [...listBackups(), ...pluginBackups()];
@@ -4240,7 +4763,7 @@ async function usage(args) {
4240
4763
  return json("usage", {
4241
4764
  usageDays: args.days,
4242
4765
  source: "Claude Code transcripts",
4243
- skills: summaries.map((s) => ({ skill: s.skill, uses: s.uses, lastUsed: s.lastUsed, projects: [...s.projects].map((p) => basename10(p)), inLibrary: library.has(s.skill) })),
4766
+ skills: summaries.map((s) => ({ skill: s.skill, uses: s.uses, lastUsed: s.lastUsed, projects: [...s.projects].map((p) => basename11(p)), inLibrary: library.has(s.skill) })),
4244
4767
  unusedLibrarySkills: unused
4245
4768
  });
4246
4769
  }
@@ -4248,13 +4771,13 @@ async function usage(args) {
4248
4771
  info(`No skill usage in Claude Code transcripts from the last ${args.days} days.`);
4249
4772
  return;
4250
4773
  }
4251
- info(`Skill usage, last ${args.days} days ${dim(`(from ${tildify(join15(claudeDir(), "projects"))})`)}
4774
+ info(`Skill usage, last ${args.days} days ${dim(`(from ${tildify(join17(claudeDir(), "projects"))})`)}
4252
4775
  `);
4253
4776
  table(
4254
4777
  summaries.map((s) => ({
4255
4778
  Skill: s.skill,
4256
4779
  Uses: String(s.uses),
4257
- Projects: [...s.projects].map((p) => basename10(p)).join(", ") || dim("-"),
4780
+ Projects: [...s.projects].map((p) => basename11(p)).join(", ") || dim("-"),
4258
4781
  "Last used": lastUsed(s),
4259
4782
  Library: library.has(s.skill) ? green("yes") : dim("no")
4260
4783
  }))
@@ -4264,9 +4787,117 @@ async function usage(args) {
4264
4787
  warn(`Library skills unused in the last ${args.days} days: ${unused.join(", ")}`);
4265
4788
  }
4266
4789
  }
4790
+ var LEVER = { plugin: "plugin", "built-in": "built-in", "claude.ai": "claude.ai", system: "system", skilllib: "skilllib" };
4791
+ async function check(args) {
4792
+ const root = projectHere();
4793
+ const usage2 = await usageBySkill(args.days);
4794
+ const kept = keptGlobal();
4795
+ const machine = machineSkills().filter((m) => !m.broken);
4796
+ const loaded = root ? skillsLoadedIn(root, machine) : machine;
4797
+ const local = root ? projectStatus(root) : [];
4798
+ const localByName = new Map(local.map((s) => [s.name, s]));
4799
+ const seen = /* @__PURE__ */ new Set();
4800
+ const rows2 = [];
4801
+ for (const g of loaded) {
4802
+ seen.add(g.name);
4803
+ const here = localByName.get(g.name);
4804
+ const u = usage2.get(g.name);
4805
+ const repoFresh = here && here.state !== "folder missing" ? freshLabel(here.state, here.version, here.latest) : null;
4806
+ const lever = LEVER[g.kind] ?? "yours";
4807
+ rows2.push({
4808
+ name: g.name,
4809
+ where: "global",
4810
+ description: g.description,
4811
+ everyPrompt: promptTokens(g.name, g.description),
4812
+ uses: u?.uses ?? 0,
4813
+ lastUsed: u ? u.lastUsed.slice(0, 10) : null,
4814
+ projects: [...u?.projects ?? []].map((p) => basename11(p)),
4815
+ fresh: lever === "yours" ? repoFresh ?? libraryFresh(g.path, g.name) : lever,
4816
+ path: g.path,
4817
+ lever,
4818
+ ...lever !== "yours" && lever !== "skilllib" ? { via: g.origin } : {},
4819
+ ...here ? { alsoInRepo: true } : {},
4820
+ ...kept.has(g.name) ? { keptGlobal: true } : {}
4821
+ });
4822
+ }
4823
+ for (const s of local) {
4824
+ if (seen.has(s.name) || s.state === "folder missing") continue;
4825
+ const u = usage2.get(s.name);
4826
+ const { description } = readSkillInfo(s.path);
4827
+ rows2.push({
4828
+ name: s.name,
4829
+ where: "repo",
4830
+ description,
4831
+ everyPrompt: promptTokens(s.name, description),
4832
+ uses: u?.uses ?? 0,
4833
+ lastUsed: u ? u.lastUsed.slice(0, 10) : null,
4834
+ projects: [...u?.projects ?? []].map((p) => basename11(p)),
4835
+ fresh: freshLabel(s.state, s.version, s.latest),
4836
+ path: s.path,
4837
+ lever: "repo"
4838
+ });
4839
+ }
4840
+ const skills = byWeight(rows2);
4841
+ const repoName = root ? basename11(root) : null;
4842
+ const brief = checkBrief(skills, { days: args.days, repo: repoName });
4843
+ const report = {
4844
+ days: args.days,
4845
+ repo: repoName,
4846
+ everyPrompt: { global: tokenSum(skills, "global"), repo: tokenSum(skills, "repo") },
4847
+ note: "Uses are Claude Code transcripts only, so 0 means it wasn't seen. everyPrompt is a rough token count (chars/4) of the name and description, which sit in every prompt. The rest of SKILL.md loads only when the skill is used. brief is the report.",
4848
+ brief,
4849
+ skills: skills.map(({ lastUsed: lastUsed2, projects: projects2, via, alsoInRepo, keptGlobal: keptGlobal2, ...rest }) => ({
4850
+ ...rest,
4851
+ ...lastUsed2 ? { lastUsed: lastUsed2 } : {},
4852
+ ...projects2.length ? { projects: projects2 } : {},
4853
+ ...via ? { via } : {},
4854
+ ...alsoInRepo ? { alsoInRepo } : {},
4855
+ ...keptGlobal2 ? { keptGlobal: keptGlobal2 } : {}
4856
+ }))
4857
+ };
4858
+ if (args.json) return json("check", report);
4859
+ if (!skills.length) return info("No skills loaded.");
4860
+ info(brief.join("\n"));
4861
+ }
4862
+ function pluginCommand(args) {
4863
+ const [action, id, extra] = args.positional;
4864
+ if (action !== "off" && action !== "on" || !id || extra) throw new Error("Usage: skilllib plugin off|on <id>");
4865
+ const on = action === "on";
4866
+ const targets = pluginTargets(id);
4867
+ const results = targets.map((p) => setPluginOn(p, on));
4868
+ const ok = results.every((r) => r.ok);
4869
+ const message = [...results.map((r) => r.message), stillOnElsewhere(targets)].filter(Boolean).join(" ");
4870
+ if (!ok) process.exitCode = 1;
4871
+ const body = { id: targets[0].id, on, ok, message };
4872
+ if (args.json) return json("plugin", body);
4873
+ if (ok) success(message);
4874
+ else error(message);
4875
+ }
4876
+ function pluginTargets(id) {
4877
+ const claude2 = installedPlugins().filter((p) => p.agent === "claude-code");
4878
+ const exact = claude2.filter((p) => p.id === id);
4879
+ const matches = exact.length ? exact : claude2.filter((p) => pluginNameOf(p.id) === id);
4880
+ if (!matches.length) {
4881
+ const cursor = cursorPlugins().some((p) => p.id === id || pluginNameOf(p.id) === id);
4882
+ throw new Error(cursor ? `${id} is a Cursor plugin. Turn it off in Cursor (Settings \u2192 Plugins).` : `No Claude Code plugin ${id}.`);
4883
+ }
4884
+ const ids = [...new Set(matches.map((p) => p.id))];
4885
+ if (ids.length > 1) throw new Error(`Several plugins match ${id}: ${ids.join(", ")}. Pass the id.`);
4886
+ const everywhere = matches.filter((p) => !p.projectPath);
4887
+ if (everywhere.length) return everywhere;
4888
+ if (matches.length === 1) return matches;
4889
+ const where = matches.map((p) => `${basename11(p.projectPath)} (${p.scope})`).join(", ");
4890
+ throw new Error(`${matches[0].id} is installed in more than one repo (${where}). This command switches the copy that loads in every repo, and these are per-repo installs.`);
4891
+ }
4892
+ function stillOnElsewhere(targets) {
4893
+ const id = targets[0].id;
4894
+ const switched = new Set(targets.map((p) => p.projectPath).filter((p) => !!p));
4895
+ const left = installedPlugins().filter((p) => p.agent === "claude-code" && p.id === id && p.projectPath && !switched.has(p.projectPath) && p.on !== false);
4896
+ return left.length ? `Still on in ${left.map((p) => `${basename11(p.projectPath)} (${p.scope})`).join(", ")}.` : "";
4897
+ }
4267
4898
  function show(args) {
4268
4899
  const name = args.positional[0];
4269
- const dir = name ? join15(libraryDir(), name) : "";
4900
+ const dir = name ? join17(libraryDir(), name) : "";
4270
4901
  if (!name || !isSkillDir(dir)) {
4271
4902
  throw new Error(name ? `${name} is not in the library` : "Usage: skilllib show <skill-name>");
4272
4903
  }
@@ -4278,8 +4909,8 @@ function show(args) {
4278
4909
  path: dir,
4279
4910
  latest: latestVersion(name)?.version ?? null,
4280
4911
  origin: libraryOrigins()[name] ?? "",
4281
- projects: projectsUsing(name).map((p) => basename10(p)),
4282
- skillMd: readFileSync7(join15(dir, "SKILL.md"), "utf-8")
4912
+ projects: projectsUsing(name).map((p) => basename11(p)),
4913
+ skillMd: readFileSync7(join17(dir, "SKILL.md"), "utf-8")
4283
4914
  });
4284
4915
  }
4285
4916
  info(`${name} ${dim(tildify(dir))}`);
@@ -4301,7 +4932,7 @@ function harnessesCommand(args) {
4301
4932
  }
4302
4933
  const enabled = enabledHarnesses();
4303
4934
  for (const h of HARNESSES) {
4304
- info(`${enabled.includes(h.id) ? green("\u25C9") : dim("\u25CB")} ${h.name.padEnd(12)} ${dim(`reads ${h.projectDirs.join(", ")}${h.installed() ? " \xB7 detected" : ""}`)}`);
4935
+ info(`${enabled.includes(h.id) ? green("\u25C9") : dim("\u25CB")} ${h.name.padEnd(18)} ${dim(`reads ${h.projectDirs.join(", ")}${h.installed() ? " \xB7 detected" : ""}`)}`);
4305
4936
  }
4306
4937
  info(dim(`
4307
4938
  New installs go to ${installDirs(enabled).join(" (real copy) + ")}${installDirs(enabled).length > 1 ? " (link)" : ""}.`));
@@ -4310,7 +4941,7 @@ function link(args) {
4310
4941
  const roots = projectsFor(args);
4311
4942
  for (const root of roots) {
4312
4943
  const res = linkAll(root, { allowTracked: args.allowTracked });
4313
- const label = args.all ? `${basename10(root)}: ` : "";
4944
+ const label = args.all ? `${basename11(root)}: ` : "";
4314
4945
  if (res.linked.length) success(`${label}linked ${res.linked.length} skill(s) for your other agents`);
4315
4946
  else if (!args.all && !res.blocked.length && !res.uncommitted.length && !res.inTheWay.length) {
4316
4947
  const missing = projectStatus(root).filter((s) => s.state === "folder missing").map((s) => s.name);
@@ -4356,11 +4987,11 @@ ${unreviewed} not reviewed. Keep one global on purpose: skilllib global keep <na
4356
4987
  }
4357
4988
  function repoRoot(repo) {
4358
4989
  const looksLikePath = /[\\/]/.test(repo) || repo.startsWith(".") || repo.startsWith("~");
4359
- const named = looksLikePath ? [] : visibleProjects().filter((p) => basename10(p) === repo);
4990
+ const named = looksLikePath ? [] : visibleProjects().filter((p) => basename11(p) === repo);
4360
4991
  if (named.length === 1) return named[0];
4361
4992
  if (named.length > 1) throw new Error(`Several projects are named ${repo}: ${named.map(tildify).join(", ")}. Pass its path.`);
4362
4993
  const path = expandHome(repo);
4363
- if (!existsSync14(path)) throw new Error(`No project ${repo} (pass its path, or see skilllib projects)`);
4994
+ if (!existsSync15(path)) throw new Error(`No project ${repo} (pass its path, or see skilllib projects)`);
4364
4995
  const root = projectHere(path);
4365
4996
  if (root !== path) throw new Error(root ? `${tildify(path)} is inside ${tildify(root)}: pass the repo's top folder` : `${tildify(path)} isn't in a git repo`);
4366
4997
  return root;
@@ -4382,22 +5013,22 @@ function moveGlobal(args, names, yours) {
4382
5013
  moved,
4383
5014
  ...reason ? { reason } : {},
4384
5015
  ...left ? { left } : {},
4385
- projects: changes.map(([root, c]) => ({ project: basename10(root), path: root, change: withDirs(root, [c])[0] }))
5016
+ projects: changes.map(([root, c]) => ({ project: basename11(root), path: root, change: withDirs(root, [c])[0] }))
4386
5017
  }))
4387
5018
  );
4388
5019
  }
4389
5020
  for (const m of moves) {
4390
- if (m.moved) success(`${m.name} now loads only in ${m.changes.map(([r]) => basename10(r)).join(", ")} (the global copy is in skilllib restore)`);
5021
+ if (m.moved) success(`${m.name} now loads only in ${m.changes.map(([r]) => basename11(r)).join(", ")} (the global copy is in skilllib restore)`);
4391
5022
  else error(`${m.name}: ${m.reason}`);
4392
- for (const [root, c] of m.changes) warnHeldLinks(root, c, `in ${basename10(root)}`);
5023
+ for (const [root, c] of m.changes) warnHeldLinks(root, c, `in ${basename11(root)}`);
4393
5024
  if (m.left) warn(`${m.name}: ${m.left.join(", ")} now point${m.left.length === 1 ? "s" : ""} at nothing (the repo's own copy serves that package): remove ${m.left.length === 1 ? "it" : "them"}`);
4394
5025
  }
4395
5026
  const toCommit = [...new Set(moves.filter((m) => m.moved).flatMap((m) => m.changes.filter(([r]) => pinsIn(r)[m.name] && !pinsIn(r)[m.name].personal).map(([r]) => r)))];
4396
- if (toCommit.length) info(dim(`Commit skilllib.json and the skill folders in ${toCommit.map((r) => basename10(r)).join(", ")}.`));
5027
+ if (toCommit.length) info(dim(`Commit skilllib.json and the skill folders in ${toCommit.map((r) => basename11(r)).join(", ")}.`));
4397
5028
  }
4398
5029
  function tidy(args) {
4399
5030
  const names = args.positional;
4400
- const keepFor = (base) => args.keep ? Object.fromEntries(names.map((n) => [n, base ? resolve8(base, args.keep) : expandHome(args.keep)])) : {};
5031
+ const keepFor = (base) => args.keep ? Object.fromEntries(names.map((n) => [n, base ? resolve9(base, args.keep) : expandHome(args.keep)])) : {};
4401
5032
  if (args.keep && !names.length) {
4402
5033
  throw new Error("Usage: skilllib tidy <name>... --keep <folder>");
4403
5034
  }
@@ -4406,13 +5037,13 @@ function tidy(args) {
4406
5037
  for (const name of names) {
4407
5038
  const where = targets.map((root) => ({ root, ...foldersOf(root, name) })).filter((w) => w.all.length);
4408
5039
  if (!where.length) {
4409
- problems.push(`${name}: no skill by that name ${args.global ? "in your global folders" : args.all ? "in your projects" : `in ${basename10(targets[0])}`}`);
5040
+ problems.push(`${name}: no skill by that name ${args.global ? "in your global folders" : args.all ? "in your projects" : `in ${basename11(targets[0])}`}`);
4410
5041
  continue;
4411
5042
  }
4412
5043
  if (!args.keep) continue;
4413
5044
  for (const { root, real } of where) {
4414
5045
  if (real.includes(keepFor(root)[name])) continue;
4415
- const label = root ? `${basename10(root)} \xB7 ` : "";
5046
+ const label = root ? `${basename11(root)} \xB7 ` : "";
4416
5047
  problems.push(
4417
5048
  `${label}${name}: no copy in ${args.keep}; ${real.length ? `pick one of: ${real.map((d) => folderLabel(root, d)).join(", ")}` : "it has no real copy to keep"}`
4418
5049
  );
@@ -4422,7 +5053,7 @@ function tidy(args) {
4422
5053
  throw new Error(problems.join("\n"));
4423
5054
  }
4424
5055
  const reports = targets.map(
4425
- (root) => root === null ? { label: "global folders", report: planGlobalTidy({ keep: keepFor(null) }) } : { label: basename10(root), report: planProjectTidy(root, { keep: keepFor(root) }) }
5056
+ (root) => root === null ? { label: "global folders", report: planGlobalTidy({ keep: keepFor(null) }) } : { label: basename11(root), report: planProjectTidy(root, { keep: keepFor(root) }) }
4426
5057
  );
4427
5058
  let held = 0;
4428
5059
  let changed = 0;
@@ -4454,7 +5085,7 @@ function tidy(args) {
4454
5085
  }
4455
5086
  if (args.dryRun) info(dim("\nNothing changed (--dry-run)."));
4456
5087
  else if (changed) info(dim(`
4457
- Replaced copies are in ${tildify(join15(skilllibHome(), "tidy-backup"))}; \`skilllib restore\` puts them back.`));
5088
+ Replaced copies are in ${tildify(join17(skilllibHome(), "tidy-backup"))}; \`skilllib restore\` puts them back.`));
4458
5089
  if (held) {
4459
5090
  process.exitCode = 1;
4460
5091
  info(dim(`${held} change${held === 1 ? "" : "s"} held back because git would see them. Re-run with --allow-git to make them.`));
@@ -4485,11 +5116,108 @@ function agentSkill(args) {
4485
5116
  else if (state === "outdated") warn("The skilllib skill is out of date. Run `skilllib agent-skill install` to update it.");
4486
5117
  else info("Your agents don't know about skilllib yet. Run `skilllib agent-skill install` so you can ask them about your skills.");
4487
5118
  }
5119
+ function installsOf(n) {
5120
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1).replace(/\.0$/, "")}M`;
5121
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1).replace(/\.0$/, "")}K`;
5122
+ return String(n);
5123
+ }
5124
+ async function search(args) {
5125
+ const query = args.positional.join(" ").trim();
5126
+ if (!query) throw new Error("Usage: skilllib search <query>");
5127
+ const hits = await searchSkills(query);
5128
+ if (args.json) return json("search", hits);
5129
+ if (!hits.length) return info(`No skills on skills.sh for \u201C${query}\u201D.`);
5130
+ table(hits.map((h) => ({ Skill: h.name, Source: h.source, Installs: dim(installsOf(h.installs)) })));
5131
+ info(dim("\nInstall one: skilllib install <source> e.g. skilllib install owner/repo@skill"));
5132
+ }
5133
+ async function install(args) {
5134
+ if (!args.positional.length) {
5135
+ throw new Error("Usage: skilllib install <owner/repo[@skill] | url | folder>... [--global] [--all] [--force]\nSearch first: skilllib search <query>");
5136
+ }
5137
+ const results = [];
5138
+ for (const input of args.positional) {
5139
+ const spec = parseInstallSpec(input);
5140
+ if (!spec) {
5141
+ const hits = await searchSkills(input).catch(() => []);
5142
+ const exact = hits.filter((h) => h.skillId === input || h.name === input);
5143
+ const pick = exact.length === 1 ? exact[0] : hits.length === 1 ? hits[0] : void 0;
5144
+ if (!pick || !pick.source.includes("/")) {
5145
+ process.exitCode = 1;
5146
+ if (args.json) results.push({ name: input, source: input, status: "skipped", reason: hits.length ? `several matches: ${hits.slice(0, 8).map((h) => `${h.source}@${h.skillId}`).join(", ")}` : "not a GitHub repo, skills.sh URL, or folder" });
5147
+ else if (hits.length) {
5148
+ warn(`\u201C${input}\u201D matches several skills. Pass one of these:`);
5149
+ for (const h of hits.slice(0, 8)) info(` skilllib install ${h.source}@${h.skillId} ${dim(installsOf(h.installs))}`);
5150
+ } else error(`${input}: not a GitHub repo, skills.sh URL, or folder. Try skilllib search ${input}`);
5151
+ continue;
5152
+ }
5153
+ args.positional.push(`${pick.source}@${pick.skillId}`);
5154
+ continue;
5155
+ }
5156
+ let resolved;
5157
+ try {
5158
+ resolved = await resolveInstallDir(spec);
5159
+ } catch (err) {
5160
+ process.exitCode = 1;
5161
+ const reason = err instanceof Error ? err.message : String(err);
5162
+ if (args.json) results.push({ name: input, source: input, status: "skipped", reason });
5163
+ else error(reason);
5164
+ continue;
5165
+ }
5166
+ try {
5167
+ if (!resolved.dirs.length) {
5168
+ process.exitCode = 1;
5169
+ const reason = "skill" in spec && spec.skill ? `no skill named ${spec.skill} in ${spec.owner}/${spec.repo}` : "no SKILL.md found";
5170
+ if (args.json) results.push({ name: input, source: resolved.source, status: "skipped", reason });
5171
+ else error(`${resolved.source}: ${reason}`);
5172
+ continue;
5173
+ }
5174
+ if (resolved.many && !args.all) {
5175
+ process.exitCode = 1;
5176
+ const names = resolved.dirs.map((d) => basename11(d));
5177
+ const reason = `${names.length} skills. Pass @name, or --all. ${names.slice(0, 12).join(", ")}`;
5178
+ if (args.json) results.push({ name: input, source: resolved.source, status: "skipped", reason });
5179
+ else {
5180
+ warn(`${resolved.source} has ${names.length} skills. Install one, or all of them:`);
5181
+ const base = "owner" in spec ? `${spec.owner}/${spec.repo}` : resolved.source;
5182
+ for (const name of names.slice(0, 12)) info(` skilllib install ${base}@${name}`);
5183
+ if (names.length > 12) info(dim(` \u2026 and ${names.length - 12} more`));
5184
+ info(dim(` skilllib install ${base} --all`));
5185
+ }
5186
+ continue;
5187
+ }
5188
+ const installed = installFound(resolved.dirs, "owner" in spec ? `${spec.owner}/${spec.repo}` : resolved.source, {
5189
+ force: args.force,
5190
+ global: args.global,
5191
+ allowTracked: args.allowTracked
5192
+ });
5193
+ results.push(...installed);
5194
+ if (!args.json) {
5195
+ for (const row of installed) {
5196
+ if (row.status === "exists" || row.status === "skipped") {
5197
+ process.exitCode = 1;
5198
+ warn(`${row.name}: ${row.reason}`);
5199
+ } else {
5200
+ const where = row.global ? `global ${row.global.map((d) => tildify(d)).join(", ")}` : row.project ? `this project (${row.project.action})` : "your library";
5201
+ success(`${row.name} ${row.status === "unchanged" ? "already in your library" : "installed"} \u2192 ${where}`);
5202
+ }
5203
+ }
5204
+ if (!args.global && !projectHere() && installed.some((r) => r.status !== "exists" && r.status !== "skipped")) {
5205
+ info(dim("Not in a project, so nothing was added here. In a repo: skilllib add <name>. Everywhere: skilllib install <source> --global"));
5206
+ }
5207
+ }
5208
+ } finally {
5209
+ if (resolved.tmp) rmSync8(resolved.tmp, { recursive: true, force: true });
5210
+ }
5211
+ }
5212
+ if (args.json) json("install", results);
5213
+ else if (results.some((r) => r.project && r.project.action !== "skipped")) info(dim("Commit skilllib.json and the skill folders."));
5214
+ }
4488
5215
 
4489
5216
  export {
4490
5217
  realPath,
4491
5218
  skilllibHome,
4492
5219
  PROJECT_SKILLS_DIR,
5220
+ FEATURED_HARNESSES,
4493
5221
  HARNESSES,
4494
5222
  harness,
4495
5223
  installDirs,
@@ -4600,10 +5328,14 @@ export {
4600
5328
  doctor,
4601
5329
  apply,
4602
5330
  usage,
5331
+ check,
5332
+ pluginCommand,
4603
5333
  show,
4604
5334
  harnessesCommand,
4605
5335
  link,
4606
5336
  globalCommand,
4607
5337
  tidy,
4608
- agentSkill
5338
+ agentSkill,
5339
+ search,
5340
+ install
4609
5341
  };