skilllib 1.12.2 → 1.13.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
@@ -639,7 +761,7 @@ function findProjects(dir, depth = 4) {
639
761
  if (depth < 0 || !existsSync5(dir)) return [];
640
762
  if (isProjectCandidate(dir)) {
641
763
  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));
764
+ const hasSkills = projectDirsInUse(enabledHarnesses()).some((d) => skillDirsIn(join6(dir, d)).length > 0) || existsSync5(join6(dir, MANIFEST_FILE));
643
765
  if (isRepo || hasSkills) return [dir];
644
766
  }
645
767
  let entries = [];
@@ -1112,7 +1234,7 @@ function blindSaid(name, blind, label = basename4) {
1112
1234
  return `${who} only ${blind.length === 1 ? "sees" : "see"} the global copy: ${fixes.join("; ")}, or keep ${name} global (skilllib global keep)`;
1113
1235
  }
1114
1236
  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)));
1237
+ const untracked = projectDirsInUse(enabledHarnesses()).find((d) => d !== GROK_SKILLS_DIR && existsSync7(join8(root, d, name, "SKILL.md")) && !isLink(join8(root, d, name)));
1116
1238
  const primary = dep ? dep.dir ?? PROJECT_SKILLS_DIR : untracked ?? installDirs(enabledHarnesses())[0];
1117
1239
  const wanted = installDirs(enabledHarnesses(), primary);
1118
1240
  const real = (d) => realpathOrNull(join8(root, d));
@@ -1177,7 +1299,7 @@ function projectStatus(root) {
1177
1299
  const lock = projectSkillsLock(root);
1178
1300
  const unmanaged = [];
1179
1301
  const byName = /* @__PURE__ */ new Map();
1180
- for (const dir of ALL_PROJECT_DIRS.filter((d) => writableDir(root, d))) {
1302
+ for (const dir of projectDirsInUse(enabled).filter((d) => writableDir(root, d))) {
1181
1303
  for (const path of skillDirsIn(join8(root, dir))) {
1182
1304
  const name = basename4(path);
1183
1305
  if (seen.has(name)) continue;
@@ -1754,12 +1876,7 @@ function shippedSkill() {
1754
1876
  return readFileSync5(new URL("../skills/skilllib/SKILL.md", import.meta.url), "utf-8");
1755
1877
  }
1756
1878
  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));
1879
+ return globalInstallDirs(enabled).map((d) => join9(d, AGENT_SKILL_NAME));
1763
1880
  }
1764
1881
  function isOurs(dir) {
1765
1882
  return existsSync8(join9(dir, OWN_SKILL_MARKER));
@@ -1790,7 +1907,7 @@ function installAgentSkill(enabled = enabledHarnesses()) {
1790
1907
  return { ok: true, dirs: [primary, ...links] };
1791
1908
  }
1792
1909
  function removeAgentSkill() {
1793
- const all = agentSkillDirs(["claude-code", "codex"]);
1910
+ const all = [...new Set(HARNESSES.flatMap((h) => h.globalDirs().map((d) => join9(d, AGENT_SKILL_NAME))))];
1794
1911
  const removed = all.filter((d) => entryExists(d) && (isOurs(d) || isDangling(d)));
1795
1912
  for (const d of removed) {
1796
1913
  if (!entryExists(d)) continue;
@@ -1807,9 +1924,9 @@ function refreshAgentSkill() {
1807
1924
  }
1808
1925
 
1809
1926
  // src/commands.ts
1810
- import { existsSync as existsSync14, readFileSync as readFileSync7 } from "fs";
1927
+ import { existsSync as existsSync15, readFileSync as readFileSync7, rmSync as rmSync7 } from "fs";
1811
1928
  import { parseArgs as parseFlags } from "util";
1812
- import { basename as basename10, join as join15, resolve as resolve8, sep as sep9 } from "path";
1929
+ import { basename as basename11, join as join16, resolve as resolve9, sep as sep10 } from "path";
1813
1930
 
1814
1931
  // src/git.ts
1815
1932
  import { execFileSync as execFileSync3 } from "child_process";
@@ -2209,11 +2326,28 @@ function namesIn(dirs) {
2209
2326
  }
2210
2327
  return [...names].sort();
2211
2328
  }
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);
2329
+ var CLASSIC_TIDY = [PROJECT_SKILLS_DIR, AGENTS_SKILLS_DIR, ".cursor/skills", ".codex/skills"];
2330
+ var tidyDirs = () => {
2331
+ const enabled = enabledHarnesses();
2332
+ const fromEnabled = enabled.flatMap((id) => harness(id).projectDirs);
2333
+ return [.../* @__PURE__ */ new Set([...CLASSIC_TIDY, ...fromEnabled])].filter((d) => d !== GROK_SKILLS_DIR && (d !== "skills" || enabled.includes("openclaw")));
2334
+ };
2335
+ var repoTidyDirs = (root) => tidyDirs().filter((d) => notWritable(root, d) === null);
2214
2336
  function globalTidyDirs() {
2215
2337
  const own = realPath(join12(grokDir(), "skills"));
2216
- return globalSkillDirs().filter((d) => realPath(d) !== own);
2338
+ const allowed = new Set(
2339
+ [
2340
+ join12(userHome(), ".agents", "skills"),
2341
+ join12(claudeDir(), "skills"),
2342
+ join12(userHome(), ".cursor", "skills"),
2343
+ join12(userHome(), ".codex", "skills"),
2344
+ ...enabledHarnesses().flatMap((id) => harness(id).globalDirs())
2345
+ ].map((d) => realPath(d))
2346
+ );
2347
+ return globalSkillDirs().filter((d) => {
2348
+ const real = realPath(d);
2349
+ return real !== own && allowed.has(real);
2350
+ });
2217
2351
  }
2218
2352
  function runsFrom(id, dirsWithCopy, readsOf) {
2219
2353
  const reads = readsOf(id).filter((d) => dirsWithCopy.includes(d));
@@ -2464,8 +2598,8 @@ function applyAll(plans, git2) {
2464
2598
  }
2465
2599
  function brokenLinksIn(root, opened) {
2466
2600
  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")));
2601
+ const tidyDirs2 = repoTidyDirs(root);
2602
+ const hasCopy = (name) => tidyDirs2.some((d) => !isLink(join13(root, d, name)) && existsSync12(join13(root, d, name, "SKILL.md")));
2469
2603
  const mends = (project, dir, name) => {
2470
2604
  const dep = pinsIn(project)[name];
2471
2605
  return !!dep && (dep.links ?? []).includes(dir) && !syncBlocker(project, name, dep) && versionForHash(name, dep.hash) !== null;
@@ -2485,7 +2619,7 @@ function brokenLinksIn(root, opened) {
2485
2619
  const seen = /* @__PURE__ */ new Set();
2486
2620
  const aliased = (dir) => (realNative(join13(root, dir)) ?? "") !== join13(realRoot, dir);
2487
2621
  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 }) => {
2622
+ const folders2 = [...projectDirsInUse(enabledHarnesses()).map((dir) => ({ dir, top: true })), ...packageFolders].flatMap(({ dir, top }) => {
2489
2623
  const real = realNative(join13(root, dir)) ?? join13(root, dir);
2490
2624
  if (seen.has(real)) return [];
2491
2625
  seen.add(real);
@@ -2867,7 +3001,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
2867
3001
  }
2868
3002
  const agentSkill2 = agentSkillState();
2869
3003
  if (agentSkill2 !== "installed") {
2870
- const install = {
3004
+ const install2 = {
2871
3005
  label: agentSkill2 === "missing" ? "Install the skilllib skill for your agents" : "Update the skilllib skill",
2872
3006
  hint: "one global skill, in ~/.claude/skills (and ~/.agents/skills)",
2873
3007
  run: () => {
@@ -2882,7 +3016,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
2882
3016
  title: agentSkill2 === "missing" ? "Your agents don't know about skilllib" : "The skilllib skill for your agents is out of date",
2883
3017
  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
3018
  at: rows(null, ["skilllib"]),
2885
- ...agentSkill2 === "missing" ? { choices: [install] } : { fix: install }
3019
+ ...agentSkill2 === "missing" ? { choices: [install2] } : { fix: install2 }
2886
3020
  });
2887
3021
  }
2888
3022
  return issues;
@@ -2959,7 +3093,7 @@ function repoIssues(root, ctx) {
2959
3093
  if (drifted.length) {
2960
3094
  const seen = drifted.some((s) => isShared(gitOf(root)?.of(relativeTo(root, s.path))));
2961
3095
  const yours = drifted.some((s) => untrackable(s));
2962
- const install = {
3096
+ const install2 = {
2963
3097
  label: "Install the pinned versions",
2964
3098
  hint: seen ? "git will see the copies change" : "your library keeps every version",
2965
3099
  run: () => summarize(where, "synced", syncProject(root, { names: drifted.map((s) => s.name) }))
@@ -2970,7 +3104,7 @@ function repoIssues(root, ctx) {
2970
3104
  title: `${where}: ${plural(drifted.length, "skill")} not at the pinned version`,
2971
3105
  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
3106
  at: rows(root, drifted.map((s) => s.name)),
2973
- ...seen || yours ? { choices: [install] } : { fix: install }
3107
+ ...seen || yours ? { choices: [install2] } : { fix: install2 }
2974
3108
  });
2975
3109
  }
2976
3110
  for (const s of unstuck.filter((s2) => s2.state === "folder missing" && s2.latest === null))
@@ -3625,12 +3759,217 @@ function usesByProject(uses, projectOf) {
3625
3759
  return byProject;
3626
3760
  }
3627
3761
 
3762
+ // src/install.ts
3763
+ import { execFileSync as execFileSync5 } from "child_process";
3764
+ import { createWriteStream, existsSync as existsSync14, lstatSync as lstatSync5, mkdirSync as mkdirSync9, mkdtempSync, readdirSync as readdirSync11, rmSync as rmSync6, statSync as statSync7 } from "fs";
3765
+ import { tmpdir as tmpdir2 } from "os";
3766
+ import { basename as basename9, join as join15, resolve as resolve8, sep as sep9 } from "path";
3767
+ import { Readable } from "stream";
3768
+ import { pipeline } from "stream/promises";
3769
+ var SEARCH_API = process.env.SKILLLIB_SKILLS_API || "https://skills.sh";
3770
+ var MAX_BYTES = 32 * 1024 * 1024;
3771
+ var SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "coverage"]);
3772
+ async function searchSkills(query) {
3773
+ const url = `${SEARCH_API}/api/search?${new URLSearchParams({ q: query, limit: "20" })}`;
3774
+ const res = await fetch(url);
3775
+ if (!res.ok) throw new Error(`skills.sh search failed (${res.status})`);
3776
+ const data = await res.json();
3777
+ return (data.skills ?? []).map((s) => ({
3778
+ name: s.name || s.skillId || s.id || "",
3779
+ source: s.source || "",
3780
+ skillId: s.skillId || s.name || "",
3781
+ installs: s.installs ?? 0
3782
+ }));
3783
+ }
3784
+ var NAME = /^[A-Za-z0-9_.-]+$/;
3785
+ function parseInstallSpec(input) {
3786
+ const trimmed = input.trim().replace(/\/+$/, "");
3787
+ if (!trimmed) return null;
3788
+ if (trimmed.startsWith(".") || trimmed.startsWith("/") || trimmed.startsWith("~") || /^[A-Za-z]:[\\/]/.test(trimmed)) {
3789
+ return { local: trimmed.replace(/^~(?=$|[\\/])/, userHome()) };
3790
+ }
3791
+ let text = trimmed;
3792
+ try {
3793
+ const url = new URL(trimmed);
3794
+ const host = url.hostname.replace(/^www\./, "");
3795
+ const parts2 = url.pathname.split("/").filter(Boolean);
3796
+ if (host === "skills.sh" && parts2.length >= 2) text = parts2.length >= 3 ? `${parts2[0]}/${parts2[1]}@${parts2.slice(2).join("/")}` : `${parts2[0]}/${parts2[1]}`;
3797
+ else if (host === "github.com" && parts2.length >= 2) {
3798
+ const repo = parts2[1].replace(/\.git$/, "");
3799
+ if (parts2[2] === "tree" && parts2.length >= 5) return namePair(parts2[0], repo, parts2.slice(4).join("/"));
3800
+ return namePair(parts2[0], repo);
3801
+ } else return null;
3802
+ } catch {
3803
+ }
3804
+ const at = text.split("@");
3805
+ if (at.length === 2 && at[0]?.includes("/") && at[1] && at[0].split("/").length === 2) {
3806
+ const [owner, repo] = at[0].split("/");
3807
+ return namePair(owner, repo, at[1]);
3808
+ }
3809
+ const parts = text.split("/");
3810
+ if (parts.length === 2) return namePair(parts[0], parts[1]);
3811
+ if (parts.length >= 3) return namePair(parts[0], parts[1], parts.slice(2).join("/"));
3812
+ return null;
3813
+ }
3814
+ function namePair(owner, repo, skill) {
3815
+ if (!NAME.test(owner) || !NAME.test(repo) || owner === "." || repo === "." || owner === ".." || repo === "..") return null;
3816
+ return skill ? { owner, repo, skill } : { owner, repo };
3817
+ }
3818
+ function escapes(root, path) {
3819
+ const base = realpathOrNull(root);
3820
+ const real = realpathOrNull(path);
3821
+ if (!base || !real) return true;
3822
+ return real !== base && !real.startsWith(base + sep9);
3823
+ }
3824
+ function findSkillDirs(root, depth = 0) {
3825
+ if (depth > 6 || !isDir(root)) return [];
3826
+ if (isSkillDir(root)) return escapes(root, root) ? [] : [root];
3827
+ let names = [];
3828
+ try {
3829
+ names = readdirSync11(root);
3830
+ } catch {
3831
+ return [];
3832
+ }
3833
+ return names.filter((n) => !n.startsWith(".") && !SKIP.has(n)).flatMap((n) => {
3834
+ const path = join15(root, n);
3835
+ const st = lstatSync5(path, { throwIfNoEntry: false });
3836
+ if (!st?.isDirectory()) return [];
3837
+ if (st.isSymbolicLink() && escapes(root, path)) return [];
3838
+ return findSkillDirs(path, depth + 1);
3839
+ });
3840
+ }
3841
+ function skillNamed(dirs, skill) {
3842
+ if (!skill) return dirs;
3843
+ const want = skill.split("/").filter(Boolean).pop();
3844
+ return dirs.filter((d) => basename9(d) === want || readSkillInfo(d).name === want);
3845
+ }
3846
+ async function downloadGitHub(owner, repo) {
3847
+ const dest = mkdtempSync(join15(tmpdir2(), "skilllib-"));
3848
+ const archive = join15(dest, "repo.tar.gz");
3849
+ const url = `https://codeload.github.com/${owner}/${repo}/tar.gz/HEAD`;
3850
+ let res;
3851
+ try {
3852
+ res = await fetch(url, { redirect: "follow" });
3853
+ } catch (err) {
3854
+ rmSync6(dest, { recursive: true, force: true });
3855
+ throw new Error(`couldn't download ${owner}/${repo} (${err instanceof Error ? err.message : err})`);
3856
+ }
3857
+ if (!res.ok || !res.body) {
3858
+ rmSync6(dest, { recursive: true, force: true });
3859
+ throw new Error(res.status === 404 ? `${owner}/${repo} not found on GitHub` : `couldn't download ${owner}/${repo} (${res.status})`);
3860
+ }
3861
+ const host = new URL(res.url).hostname;
3862
+ if (host !== "codeload.github.com" && host !== "github.com") {
3863
+ rmSync6(dest, { recursive: true, force: true });
3864
+ throw new Error(`refusing to download ${owner}/${repo} from ${host}`);
3865
+ }
3866
+ let bytes = 0;
3867
+ try {
3868
+ await pipeline(
3869
+ Readable.from(
3870
+ (async function* () {
3871
+ const reader = res.body.getReader();
3872
+ for (; ; ) {
3873
+ const { done, value } = await reader.read();
3874
+ if (done) return;
3875
+ bytes += value.byteLength;
3876
+ if (bytes > MAX_BYTES) throw new Error(`${owner}/${repo} is larger than ${MAX_BYTES / 1024 / 1024}MB`);
3877
+ yield value;
3878
+ }
3879
+ })()
3880
+ ),
3881
+ createWriteStream(archive)
3882
+ );
3883
+ execFileSync5("tar", ["-xzf", archive, "-C", dest], { stdio: "ignore" });
3884
+ } catch (err) {
3885
+ rmSync6(dest, { recursive: true, force: true });
3886
+ if (err instanceof Error && /larger than|couldn't download/.test(err.message)) throw err;
3887
+ throw new Error(`couldn't extract ${owner}/${repo}`);
3888
+ }
3889
+ rmSync6(archive, { force: true });
3890
+ const top = readdirSync11(dest);
3891
+ const root = top.length === 1 && statSync7(join15(dest, top[0])).isDirectory() ? join15(dest, top[0]) : dest;
3892
+ return { tmp: dest, root };
3893
+ }
3894
+ function installGlobal(name, { force = false } = {}) {
3895
+ const latest = latestVersion(name);
3896
+ if (!latest) return { ok: false, reason: "not in the library" };
3897
+ const parents = globalInstallDirs(enabledHarnesses());
3898
+ if (!parents.length) return { ok: false, reason: "no agents chosen yet (skilllib harnesses)" };
3899
+ const [primary, ...links] = parents;
3900
+ const dest = join15(primary, name);
3901
+ if (entryExists(dest) && !isLink(dest) && !isDir(dest)) return { ok: false, reason: `${dest} is a file` };
3902
+ const there = treeHash(dest);
3903
+ if (there && there !== latest.hash && versionForHash(name, there) === null && !force) {
3904
+ return { ok: false, reason: `${name} is already global, with edits that aren't in your library (pass --force to replace them)` };
3905
+ }
3906
+ if (there !== latest.hash) {
3907
+ if (isLink(dest)) rmSync6(dest, { force: true });
3908
+ mkdirSync9(primary, { recursive: true });
3909
+ copySkill(versionDir(name, latest.version), dest);
3910
+ }
3911
+ const wrote = [dest];
3912
+ for (const dir of links) {
3913
+ const link2 = join15(dir, name);
3914
+ if (isLink(link2) && realpathOrNull(link2) === null) rmSync6(link2, { force: true });
3915
+ else if (entryExists(link2)) {
3916
+ if (realpathOrNull(link2) === realpathOrNull(dest)) wrote.push(link2);
3917
+ continue;
3918
+ }
3919
+ mkdirSync9(dir, { recursive: true });
3920
+ linkDir(realpathOrNull(dest) ?? dest, join15(realpathOrNull(dir) ?? dir, name));
3921
+ wrote.push(link2);
3922
+ }
3923
+ setKeepGlobal([name], true);
3924
+ return { ok: true, dirs: wrote };
3925
+ }
3926
+ function installFound(dirs, source, { force = false, global = false, allowTracked = false } = {}) {
3927
+ const root = global ? null : projectHere();
3928
+ const label = source.startsWith("/") || source.startsWith(".") ? source : `skills.sh: ${source}`;
3929
+ return dirs.map((dir) => {
3930
+ const imported = importSkill(dir, { force });
3931
+ if (imported.status !== "exists") recordOrigin(imported.name, label);
3932
+ const row = { name: imported.name, source, status: imported.status };
3933
+ if (imported.status === "exists") {
3934
+ row.reason = "the library already has a different version (pass --force)";
3935
+ return row;
3936
+ }
3937
+ if (global) {
3938
+ const placed = installGlobal(imported.name, { force });
3939
+ if (!placed.ok) {
3940
+ row.status = "skipped";
3941
+ row.reason = placed.reason;
3942
+ } else row.global = placed.dirs;
3943
+ return row;
3944
+ }
3945
+ if (!root) return row;
3946
+ row.project = addSkill(root, imported.name, { force, allowTracked });
3947
+ if (row.project.action === "skipped") {
3948
+ row.status = "skipped";
3949
+ row.reason = row.project.reason;
3950
+ }
3951
+ return row;
3952
+ });
3953
+ }
3954
+ async function resolveInstallDir(spec) {
3955
+ if ("local" in spec) {
3956
+ const path = resolve8(spec.local);
3957
+ if (!existsSync14(path)) throw new Error(`${path} doesn't exist`);
3958
+ const dirs2 = isSkillDir(path) ? [path] : findSkillDirs(path);
3959
+ return { dirs: dirs2, source: path, many: dirs2.length > 1 };
3960
+ }
3961
+ const got = await downloadGitHub(spec.owner, spec.repo);
3962
+ const source = spec.skill ? `${spec.owner}/${spec.repo}@${spec.skill}` : `${spec.owner}/${spec.repo}`;
3963
+ const dirs = skillNamed(findSkillDirs(got.root), spec.skill);
3964
+ return { tmp: got.tmp, dirs, source, many: !spec.skill && dirs.length > 1 };
3965
+ }
3966
+
3628
3967
  // src/move.ts
3629
- import { basename as basename9 } from "path";
3968
+ import { basename as basename10 } from "path";
3630
3969
  function moveGlobalSkills(skills, roots, {
3631
3970
  group,
3632
3971
  allowTracked = false,
3633
- label = basename9,
3972
+ label = basename10,
3634
3973
  allowTrackedHint = "re-run with --allow-tracked"
3635
3974
  } = {}) {
3636
3975
  const byName = /* @__PURE__ */ new Map();
@@ -3749,9 +4088,9 @@ function projectOfFactory(projects2) {
3749
4088
  const sorted2 = [...projects2].sort((a, b) => b.length - a.length);
3750
4089
  return (cwd) => {
3751
4090
  if (!cwd) return null;
3752
- const known = sorted2.find((p) => cwd === p || cwd.startsWith(p + sep9));
4091
+ const known = sorted2.find((p) => cwd === p || cwd.startsWith(p + sep10));
3753
4092
  if (known) return known;
3754
- const repo = cwd.split(`${sep9}.claude${sep9}worktrees${sep9}`)[0];
4093
+ const repo = cwd.split(`${sep10}.claude${sep10}worktrees${sep10}`)[0];
3755
4094
  return isProjectCandidate(repo) ? repo : null;
3756
4095
  };
3757
4096
  }
@@ -3765,7 +4104,7 @@ var projectsFor = (args) => {
3765
4104
  if (!args.all) return [requireProject()];
3766
4105
  const { readable, unreadable } = readableRoots(visibleProjects());
3767
4106
  for (const u of unreadable) {
3768
- error(`${basename10(u.root)}: ${u.reason}`);
4107
+ error(`${basename11(u.root)}: ${u.reason}`);
3769
4108
  process.exitCode = 1;
3770
4109
  }
3771
4110
  return readable;
@@ -3777,7 +4116,7 @@ function requireProject() {
3777
4116
  if (!isProjectCandidate(findProjectRoot())) {
3778
4117
  const top = findProjectRoot();
3779
4118
  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` : "";
4119
+ const stray = existsSync15(join16(userHome(), MANIFEST_FILE)) ? `, though ${tildify(join16(userHome(), MANIFEST_FILE))} makes it look like one` : "";
3781
4120
  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
4121
  }
3783
4122
  throw new Error("Not in a git repo. Run this inside a project (or `git init` first).");
@@ -3799,7 +4138,7 @@ async function status(args) {
3799
4138
  const root = requireProject();
3800
4139
  const skills = projectStatus(root);
3801
4140
  const nested = nestedSkills(root);
3802
- info(`${basename10(root)} ${dim(tildify(root))}
4141
+ info(`${basename11(root)} ${dim(tildify(root))}
3803
4142
  `);
3804
4143
  if (skills.length === 0 && nested.length === 0) info("No skills in this project yet. Add one from your library with: skilllib add <name>");
3805
4144
  const usage2 = await usageBySkill(args.days, root);
@@ -3872,7 +4211,7 @@ async function list(args) {
3872
4211
  const installs = /* @__PURE__ */ new Map();
3873
4212
  for (const project of visibleProjects()) {
3874
4213
  for (const name of Object.keys(pinsIn(project))) {
3875
- installs.set(name, [...installs.get(name) ?? [], basename10(project)]);
4214
+ installs.set(name, [...installs.get(name) ?? [], basename11(project)]);
3876
4215
  }
3877
4216
  }
3878
4217
  const usage2 = await usageBySkill(args.days);
@@ -3915,7 +4254,7 @@ function teamStateIn(root, extraDirs = []) {
3915
4254
  function projects(args) {
3916
4255
  const { readable: roots, unreadable } = readableRoots(visibleProjects());
3917
4256
  for (const u of unreadable) {
3918
- error(`${basename10(u.root)}: ${u.reason}`);
4257
+ error(`${basename11(u.root)}: ${u.reason}`);
3919
4258
  process.exitCode = 1;
3920
4259
  }
3921
4260
  if (args.json) {
@@ -3924,7 +4263,7 @@ function projects(args) {
3924
4263
  roots.map((root) => {
3925
4264
  const teamState = teamStateIn(root);
3926
4265
  return {
3927
- name: basename10(root),
4266
+ name: basename11(root),
3928
4267
  path: root,
3929
4268
  skills: projectStatus(root).map((s) => ({ name: s.name, managed: s.managed, state: teamState(s), version: s.version, latest: s.latest }))
3930
4269
  };
@@ -3942,7 +4281,7 @@ function projects(args) {
3942
4281
  const usual = /* @__PURE__ */ new Set(["ok", "local only", "repo skill", "repo skill, in library", "from npx skills"]);
3943
4282
  const attention = skills.filter((s) => !usual.has(teamState(s))).length;
3944
4283
  return {
3945
- Project: basename10(root),
4284
+ Project: basename11(root),
3946
4285
  Path: dim(tildify(root)),
3947
4286
  Skills: skills.map((s) => s.name).join(", ") || dim("none"),
3948
4287
  "Local only": String(skills.filter((s) => teamState(s) === "local only").length),
@@ -3977,11 +4316,11 @@ function changeEach(args, change, nothing) {
3977
4316
  const changes = change(root);
3978
4317
  if (changes.some(incomplete)) process.exitCode = 1;
3979
4318
  if (!args.json) {
3980
- if (args.all) info(`${basename10(root)} ${dim(tildify(root))}`);
4319
+ if (args.all) info(`${basename11(root)} ${dim(tildify(root))}`);
3981
4320
  if (changes.length === 0) nothing(root);
3982
4321
  else printChanges(root, changes, args);
3983
4322
  }
3984
- return { project: basename10(root), path: root, changes: withDirs(root, changes) };
4323
+ return { project: basename11(root), path: root, changes: withDirs(root, changes) };
3985
4324
  });
3986
4325
  if (args.json) args.all ? json("changes --all", results) : json("changes", results[0].changes);
3987
4326
  return roots;
@@ -4012,7 +4351,7 @@ function update(args) {
4012
4351
  function outdated(args) {
4013
4352
  const roots = projectsFor(args);
4014
4353
  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 }))
4354
+ (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
4355
  );
4017
4356
  if (args.json) return json("outdated", behind);
4018
4357
  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 +4362,7 @@ function outdated(args) {
4023
4362
  }
4024
4363
  function importSkills(args) {
4025
4364
  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));
4365
+ const dirs = args.global ? yourGlobals().map((m) => m.path) : args.positional.map((p) => resolve9(p));
4027
4366
  if (dirs.length === 0) {
4028
4367
  throw new Error("Usage: skilllib import <skill-dir>... [--force] | skilllib import --global");
4029
4368
  }
@@ -4043,11 +4382,11 @@ function importSkills(args) {
4043
4382
  continue;
4044
4383
  }
4045
4384
  success(`${name} ${dim(result === "unchanged" ? "already in library" : result === "added" ? "added to library" : "library updated")}`);
4046
- const root = resolve8(dir, "..", "..", "..");
4385
+ const root = resolve9(dir, "..", "..", "..");
4047
4386
  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)) {
4387
+ if (dep && realpathOrNull(join16(root, dep.dir ?? PROJECT_SKILLS_DIR, name)) === realpathOrNull(dir) && dep.hash !== treeHash(dir)) {
4049
4388
  const change = addSkill(root, name);
4050
- if (change.action !== "skipped") info(dim(` ${basename10(root)} now depends on ${name} v${change.to}`));
4389
+ if (change.action !== "skipped") info(dim(` ${basename11(root)} now depends on ${name} v${change.to}`));
4051
4390
  }
4052
4391
  }
4053
4392
  if (args.json) json("import", results);
@@ -4063,14 +4402,14 @@ function scan(args) {
4063
4402
  const library = new Set(librarySkills().map((s) => s.name));
4064
4403
  const skillNames = (root) => [
4065
4404
  // 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)])
4405
+ .../* @__PURE__ */ new Set([...projectDirsInUse(enabledHarnesses()).filter((d) => !notWritable(root, d)).flatMap((d) => skillDirsIn(join16(root, d))).map((d) => basename11(d)), ...nestedSkills(root).map((s) => s.name)])
4067
4406
  ];
4068
4407
  const withSkills = found.filter((root) => skillNames(root).length > 0);
4069
4408
  info(`Scanned ${roots.map((r) => tildify(r)).join(", ")}: ${found.length} project${found.length === 1 ? "" : "s"}, ${withSkills.length} with skills
4070
4409
  `);
4071
4410
  table(
4072
4411
  withSkills.map((root) => ({
4073
- Project: basename10(root),
4412
+ Project: basename11(root),
4074
4413
  Path: dim(tildify(root)),
4075
4414
  Skills: skillNames(root).map((n) => library.has(n) ? n : yellow(n)).join(", ")
4076
4415
  }))
@@ -4127,7 +4466,7 @@ function newSkill(args) {
4127
4466
  if (!r.ok) {
4128
4467
  throw new Error(`Can't create ${name}: ${r.reason}`);
4129
4468
  }
4130
- success(`Created ${tildify(join15(r.dir, "SKILL.md"))}`);
4469
+ success(`Created ${tildify(join16(r.dir, "SKILL.md"))}`);
4131
4470
  }
4132
4471
  function restore(args) {
4133
4472
  const backups = [...listBackups(), ...pluginBackups()];
@@ -4240,7 +4579,7 @@ async function usage(args) {
4240
4579
  return json("usage", {
4241
4580
  usageDays: args.days,
4242
4581
  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) })),
4582
+ 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
4583
  unusedLibrarySkills: unused
4245
4584
  });
4246
4585
  }
@@ -4248,13 +4587,13 @@ async function usage(args) {
4248
4587
  info(`No skill usage in Claude Code transcripts from the last ${args.days} days.`);
4249
4588
  return;
4250
4589
  }
4251
- info(`Skill usage, last ${args.days} days ${dim(`(from ${tildify(join15(claudeDir(), "projects"))})`)}
4590
+ info(`Skill usage, last ${args.days} days ${dim(`(from ${tildify(join16(claudeDir(), "projects"))})`)}
4252
4591
  `);
4253
4592
  table(
4254
4593
  summaries.map((s) => ({
4255
4594
  Skill: s.skill,
4256
4595
  Uses: String(s.uses),
4257
- Projects: [...s.projects].map((p) => basename10(p)).join(", ") || dim("-"),
4596
+ Projects: [...s.projects].map((p) => basename11(p)).join(", ") || dim("-"),
4258
4597
  "Last used": lastUsed(s),
4259
4598
  Library: library.has(s.skill) ? green("yes") : dim("no")
4260
4599
  }))
@@ -4266,7 +4605,7 @@ async function usage(args) {
4266
4605
  }
4267
4606
  function show(args) {
4268
4607
  const name = args.positional[0];
4269
- const dir = name ? join15(libraryDir(), name) : "";
4608
+ const dir = name ? join16(libraryDir(), name) : "";
4270
4609
  if (!name || !isSkillDir(dir)) {
4271
4610
  throw new Error(name ? `${name} is not in the library` : "Usage: skilllib show <skill-name>");
4272
4611
  }
@@ -4278,8 +4617,8 @@ function show(args) {
4278
4617
  path: dir,
4279
4618
  latest: latestVersion(name)?.version ?? null,
4280
4619
  origin: libraryOrigins()[name] ?? "",
4281
- projects: projectsUsing(name).map((p) => basename10(p)),
4282
- skillMd: readFileSync7(join15(dir, "SKILL.md"), "utf-8")
4620
+ projects: projectsUsing(name).map((p) => basename11(p)),
4621
+ skillMd: readFileSync7(join16(dir, "SKILL.md"), "utf-8")
4283
4622
  });
4284
4623
  }
4285
4624
  info(`${name} ${dim(tildify(dir))}`);
@@ -4301,7 +4640,7 @@ function harnessesCommand(args) {
4301
4640
  }
4302
4641
  const enabled = enabledHarnesses();
4303
4642
  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" : ""}`)}`);
4643
+ info(`${enabled.includes(h.id) ? green("\u25C9") : dim("\u25CB")} ${h.name.padEnd(18)} ${dim(`reads ${h.projectDirs.join(", ")}${h.installed() ? " \xB7 detected" : ""}`)}`);
4305
4644
  }
4306
4645
  info(dim(`
4307
4646
  New installs go to ${installDirs(enabled).join(" (real copy) + ")}${installDirs(enabled).length > 1 ? " (link)" : ""}.`));
@@ -4310,7 +4649,7 @@ function link(args) {
4310
4649
  const roots = projectsFor(args);
4311
4650
  for (const root of roots) {
4312
4651
  const res = linkAll(root, { allowTracked: args.allowTracked });
4313
- const label = args.all ? `${basename10(root)}: ` : "";
4652
+ const label = args.all ? `${basename11(root)}: ` : "";
4314
4653
  if (res.linked.length) success(`${label}linked ${res.linked.length} skill(s) for your other agents`);
4315
4654
  else if (!args.all && !res.blocked.length && !res.uncommitted.length && !res.inTheWay.length) {
4316
4655
  const missing = projectStatus(root).filter((s) => s.state === "folder missing").map((s) => s.name);
@@ -4356,11 +4695,11 @@ ${unreviewed} not reviewed. Keep one global on purpose: skilllib global keep <na
4356
4695
  }
4357
4696
  function repoRoot(repo) {
4358
4697
  const looksLikePath = /[\\/]/.test(repo) || repo.startsWith(".") || repo.startsWith("~");
4359
- const named = looksLikePath ? [] : visibleProjects().filter((p) => basename10(p) === repo);
4698
+ const named = looksLikePath ? [] : visibleProjects().filter((p) => basename11(p) === repo);
4360
4699
  if (named.length === 1) return named[0];
4361
4700
  if (named.length > 1) throw new Error(`Several projects are named ${repo}: ${named.map(tildify).join(", ")}. Pass its path.`);
4362
4701
  const path = expandHome(repo);
4363
- if (!existsSync14(path)) throw new Error(`No project ${repo} (pass its path, or see skilllib projects)`);
4702
+ if (!existsSync15(path)) throw new Error(`No project ${repo} (pass its path, or see skilllib projects)`);
4364
4703
  const root = projectHere(path);
4365
4704
  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
4705
  return root;
@@ -4382,22 +4721,22 @@ function moveGlobal(args, names, yours) {
4382
4721
  moved,
4383
4722
  ...reason ? { reason } : {},
4384
4723
  ...left ? { left } : {},
4385
- projects: changes.map(([root, c]) => ({ project: basename10(root), path: root, change: withDirs(root, [c])[0] }))
4724
+ projects: changes.map(([root, c]) => ({ project: basename11(root), path: root, change: withDirs(root, [c])[0] }))
4386
4725
  }))
4387
4726
  );
4388
4727
  }
4389
4728
  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)`);
4729
+ 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
4730
  else error(`${m.name}: ${m.reason}`);
4392
- for (const [root, c] of m.changes) warnHeldLinks(root, c, `in ${basename10(root)}`);
4731
+ for (const [root, c] of m.changes) warnHeldLinks(root, c, `in ${basename11(root)}`);
4393
4732
  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
4733
  }
4395
4734
  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(", ")}.`));
4735
+ if (toCommit.length) info(dim(`Commit skilllib.json and the skill folders in ${toCommit.map((r) => basename11(r)).join(", ")}.`));
4397
4736
  }
4398
4737
  function tidy(args) {
4399
4738
  const names = args.positional;
4400
- const keepFor = (base) => args.keep ? Object.fromEntries(names.map((n) => [n, base ? resolve8(base, args.keep) : expandHome(args.keep)])) : {};
4739
+ const keepFor = (base) => args.keep ? Object.fromEntries(names.map((n) => [n, base ? resolve9(base, args.keep) : expandHome(args.keep)])) : {};
4401
4740
  if (args.keep && !names.length) {
4402
4741
  throw new Error("Usage: skilllib tidy <name>... --keep <folder>");
4403
4742
  }
@@ -4406,13 +4745,13 @@ function tidy(args) {
4406
4745
  for (const name of names) {
4407
4746
  const where = targets.map((root) => ({ root, ...foldersOf(root, name) })).filter((w) => w.all.length);
4408
4747
  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])}`}`);
4748
+ problems.push(`${name}: no skill by that name ${args.global ? "in your global folders" : args.all ? "in your projects" : `in ${basename11(targets[0])}`}`);
4410
4749
  continue;
4411
4750
  }
4412
4751
  if (!args.keep) continue;
4413
4752
  for (const { root, real } of where) {
4414
4753
  if (real.includes(keepFor(root)[name])) continue;
4415
- const label = root ? `${basename10(root)} \xB7 ` : "";
4754
+ const label = root ? `${basename11(root)} \xB7 ` : "";
4416
4755
  problems.push(
4417
4756
  `${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
4757
  );
@@ -4422,7 +4761,7 @@ function tidy(args) {
4422
4761
  throw new Error(problems.join("\n"));
4423
4762
  }
4424
4763
  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) }) }
4764
+ (root) => root === null ? { label: "global folders", report: planGlobalTidy({ keep: keepFor(null) }) } : { label: basename11(root), report: planProjectTidy(root, { keep: keepFor(root) }) }
4426
4765
  );
4427
4766
  let held = 0;
4428
4767
  let changed = 0;
@@ -4454,7 +4793,7 @@ function tidy(args) {
4454
4793
  }
4455
4794
  if (args.dryRun) info(dim("\nNothing changed (--dry-run)."));
4456
4795
  else if (changed) info(dim(`
4457
- Replaced copies are in ${tildify(join15(skilllibHome(), "tidy-backup"))}; \`skilllib restore\` puts them back.`));
4796
+ Replaced copies are in ${tildify(join16(skilllibHome(), "tidy-backup"))}; \`skilllib restore\` puts them back.`));
4458
4797
  if (held) {
4459
4798
  process.exitCode = 1;
4460
4799
  info(dim(`${held} change${held === 1 ? "" : "s"} held back because git would see them. Re-run with --allow-git to make them.`));
@@ -4485,11 +4824,108 @@ function agentSkill(args) {
4485
4824
  else if (state === "outdated") warn("The skilllib skill is out of date. Run `skilllib agent-skill install` to update it.");
4486
4825
  else info("Your agents don't know about skilllib yet. Run `skilllib agent-skill install` so you can ask them about your skills.");
4487
4826
  }
4827
+ function installsOf(n) {
4828
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1).replace(/\.0$/, "")}M`;
4829
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1).replace(/\.0$/, "")}K`;
4830
+ return String(n);
4831
+ }
4832
+ async function search(args) {
4833
+ const query = args.positional.join(" ").trim();
4834
+ if (!query) throw new Error("Usage: skilllib search <query>");
4835
+ const hits = await searchSkills(query);
4836
+ if (args.json) return json("search", hits);
4837
+ if (!hits.length) return info(`No skills on skills.sh for \u201C${query}\u201D.`);
4838
+ table(hits.map((h) => ({ Skill: h.name, Source: h.source, Installs: dim(installsOf(h.installs)) })));
4839
+ info(dim("\nInstall one: skilllib install <source> e.g. skilllib install owner/repo@skill"));
4840
+ }
4841
+ async function install(args) {
4842
+ if (!args.positional.length) {
4843
+ throw new Error("Usage: skilllib install <owner/repo[@skill] | url | folder>... [--global] [--all] [--force]\nSearch first: skilllib search <query>");
4844
+ }
4845
+ const results = [];
4846
+ for (const input of args.positional) {
4847
+ const spec = parseInstallSpec(input);
4848
+ if (!spec) {
4849
+ const hits = await searchSkills(input).catch(() => []);
4850
+ const exact = hits.filter((h) => h.skillId === input || h.name === input);
4851
+ const pick = exact.length === 1 ? exact[0] : hits.length === 1 ? hits[0] : void 0;
4852
+ if (!pick || !pick.source.includes("/")) {
4853
+ process.exitCode = 1;
4854
+ 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" });
4855
+ else if (hits.length) {
4856
+ warn(`\u201C${input}\u201D matches several skills. Pass one of these:`);
4857
+ for (const h of hits.slice(0, 8)) info(` skilllib install ${h.source}@${h.skillId} ${dim(installsOf(h.installs))}`);
4858
+ } else error(`${input}: not a GitHub repo, skills.sh URL, or folder. Try skilllib search ${input}`);
4859
+ continue;
4860
+ }
4861
+ args.positional.push(`${pick.source}@${pick.skillId}`);
4862
+ continue;
4863
+ }
4864
+ let resolved;
4865
+ try {
4866
+ resolved = await resolveInstallDir(spec);
4867
+ } catch (err) {
4868
+ process.exitCode = 1;
4869
+ const reason = err instanceof Error ? err.message : String(err);
4870
+ if (args.json) results.push({ name: input, source: input, status: "skipped", reason });
4871
+ else error(reason);
4872
+ continue;
4873
+ }
4874
+ try {
4875
+ if (!resolved.dirs.length) {
4876
+ process.exitCode = 1;
4877
+ const reason = "skill" in spec && spec.skill ? `no skill named ${spec.skill} in ${spec.owner}/${spec.repo}` : "no SKILL.md found";
4878
+ if (args.json) results.push({ name: input, source: resolved.source, status: "skipped", reason });
4879
+ else error(`${resolved.source}: ${reason}`);
4880
+ continue;
4881
+ }
4882
+ if (resolved.many && !args.all) {
4883
+ process.exitCode = 1;
4884
+ const names = resolved.dirs.map((d) => basename11(d));
4885
+ const reason = `${names.length} skills. Pass @name, or --all. ${names.slice(0, 12).join(", ")}`;
4886
+ if (args.json) results.push({ name: input, source: resolved.source, status: "skipped", reason });
4887
+ else {
4888
+ warn(`${resolved.source} has ${names.length} skills. Install one, or all of them:`);
4889
+ const base = "owner" in spec ? `${spec.owner}/${spec.repo}` : resolved.source;
4890
+ for (const name of names.slice(0, 12)) info(` skilllib install ${base}@${name}`);
4891
+ if (names.length > 12) info(dim(` \u2026 and ${names.length - 12} more`));
4892
+ info(dim(` skilllib install ${base} --all`));
4893
+ }
4894
+ continue;
4895
+ }
4896
+ const installed = installFound(resolved.dirs, "owner" in spec ? `${spec.owner}/${spec.repo}` : resolved.source, {
4897
+ force: args.force,
4898
+ global: args.global,
4899
+ allowTracked: args.allowTracked
4900
+ });
4901
+ results.push(...installed);
4902
+ if (!args.json) {
4903
+ for (const row of installed) {
4904
+ if (row.status === "exists" || row.status === "skipped") {
4905
+ process.exitCode = 1;
4906
+ warn(`${row.name}: ${row.reason}`);
4907
+ } else {
4908
+ const where = row.global ? `global ${row.global.map((d) => tildify(d)).join(", ")}` : row.project ? `this project (${row.project.action})` : "your library";
4909
+ success(`${row.name} ${row.status === "unchanged" ? "already in your library" : "installed"} \u2192 ${where}`);
4910
+ }
4911
+ }
4912
+ if (!args.global && !projectHere() && installed.some((r) => r.status !== "exists" && r.status !== "skipped")) {
4913
+ info(dim("Not in a project, so nothing was added here. In a repo: skilllib add <name>. Everywhere: skilllib install <source> --global"));
4914
+ }
4915
+ }
4916
+ } finally {
4917
+ if (resolved.tmp) rmSync7(resolved.tmp, { recursive: true, force: true });
4918
+ }
4919
+ }
4920
+ if (args.json) json("install", results);
4921
+ else if (results.some((r) => r.project && r.project.action !== "skipped")) info(dim("Commit skilllib.json and the skill folders."));
4922
+ }
4488
4923
 
4489
4924
  export {
4490
4925
  realPath,
4491
4926
  skilllibHome,
4492
4927
  PROJECT_SKILLS_DIR,
4928
+ FEATURED_HARNESSES,
4493
4929
  HARNESSES,
4494
4930
  harness,
4495
4931
  installDirs,
@@ -4605,5 +5041,7 @@ export {
4605
5041
  link,
4606
5042
  globalCommand,
4607
5043
  tidy,
4608
- agentSkill
5044
+ agentSkill,
5045
+ search,
5046
+ install
4609
5047
  };