skilllib 1.12.1 → 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
@@ -478,7 +600,7 @@ function repoOf(root) {
478
600
  const seen = changedAt(config);
479
601
  let repo = null;
480
602
  if (out2[0] === "") {
481
- const urls = (gitIn(root, ["config", "--get-regexp", "^remote\\..*\\.url$"]) ?? "").split("\n").filter(Boolean).map((l) => l.split(" "));
603
+ 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
604
  const url = (urls.find(([k]) => k === "remote.origin.url") ?? urls[0])?.[1];
483
605
  const remote = url ? normalizeRemote(url) : void 0;
484
606
  repo = { file: pinsFileFor(root, remote, folder), folder, common, ...remote ? { remote } : {}, root };
@@ -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;
@@ -1395,8 +1517,8 @@ function addSkill(root, name, { force = false, version, allowTracked = false, pe
1395
1517
  if (moving && !keep) return { name, action: "skipped", reason: `your library has no copy of the version it pins, so its pin can't move as it is: skilllib update ${name} first` };
1396
1518
  const target = keep ?? (version === void 0 ? latestVersion(name) : getVersion(name, version));
1397
1519
  if (!target) return { name, action: "skipped", reason: version === void 0 ? "not in the library" : `version ${version} doesn't exist` };
1398
- const plan = addPlan(root, name, target.hash, force);
1399
- if ("reason" in plan) return { name, action: "skipped", reason: plan.reason };
1520
+ const plan = addPlan(root, name, target.hash, moving ? false : force);
1521
+ if ("reason" in plan) return { name, action: "skipped", reason: moving ? `${withoutForceHint(plan.reason)}, so its pin can't move: save the edits to your library first` : plan.reason };
1400
1522
  const { manifest, recorded, primary, wantedLinks, to, local } = plan;
1401
1523
  const mine = pinsWhere(root, recorded, personal);
1402
1524
  if (typeof mine === "string") return { name, action: "skipped", reason: mine };
@@ -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));
@@ -2400,6 +2534,8 @@ function intoLibrary(path, force = false) {
2400
2534
  if (r.status === "exists") throw new Error(`${r.name}: your library has a different ${r.name}; update or delete it first`);
2401
2535
  return r.name;
2402
2536
  }
2537
+ var pinsOf = (where, personal) => personal ? "your personal pins" : `${where}'s skilllib.json`;
2538
+ var newPinsOf = (root, where) => pinsOf(where, !existsSync12(join13(root, MANIFEST_FILE)) && personalPinsFile(root) !== null);
2403
2539
  function track(root, name) {
2404
2540
  const c = addSkill(root, name);
2405
2541
  if (c.action === "skipped") throw new Error(`${name}: ${c.reason}`);
@@ -2462,8 +2598,8 @@ function applyAll(plans, git2) {
2462
2598
  }
2463
2599
  function brokenLinksIn(root, opened) {
2464
2600
  const where = basename7(root);
2465
- const tidyDirs = repoTidyDirs(root);
2466
- 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")));
2467
2603
  const mends = (project, dir, name) => {
2468
2604
  const dep = pinsIn(project)[name];
2469
2605
  return !!dep && (dep.links ?? []).includes(dir) && !syncBlocker(project, name, dep) && versionForHash(name, dep.hash) !== null;
@@ -2483,7 +2619,7 @@ function brokenLinksIn(root, opened) {
2483
2619
  const seen = /* @__PURE__ */ new Set();
2484
2620
  const aliased = (dir) => (realNative(join13(root, dir)) ?? "") !== join13(realRoot, dir);
2485
2621
  const packageFolders = packageSkillFolders(root).map((f) => ({ dir: f.location, top: false })).sort((a, b) => Number(aliased(a.dir)) - Number(aliased(b.dir)));
2486
- 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 }) => {
2487
2623
  const real = realNative(join13(root, dir)) ?? join13(root, dir);
2488
2624
  if (seen.has(real)) return [];
2489
2625
  seen.add(real);
@@ -2865,7 +3001,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
2865
3001
  }
2866
3002
  const agentSkill2 = agentSkillState();
2867
3003
  if (agentSkill2 !== "installed") {
2868
- const install = {
3004
+ const install2 = {
2869
3005
  label: agentSkill2 === "missing" ? "Install the skilllib skill for your agents" : "Update the skilllib skill",
2870
3006
  hint: "one global skill, in ~/.claude/skills (and ~/.agents/skills)",
2871
3007
  run: () => {
@@ -2880,7 +3016,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
2880
3016
  title: agentSkill2 === "missing" ? "Your agents don't know about skilllib" : "The skilllib skill for your agents is out of date",
2881
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.",
2882
3018
  at: rows(null, ["skilllib"]),
2883
- ...agentSkill2 === "missing" ? { choices: [install] } : { fix: install }
3019
+ ...agentSkill2 === "missing" ? { choices: [install2] } : { fix: install2 }
2884
3020
  });
2885
3021
  }
2886
3022
  return issues;
@@ -2937,7 +3073,7 @@ function repoIssues(root, ctx) {
2937
3073
  detail: `${s.state === "folder missing" ? "The folder is missing, and no" : "The copy is another version, and no"} version in Your skills has the content it pins (pinned from another library?). ${untrackable(s) ? `${untrackable(s)}, so skilllib can't install it here.` : `Install your newest v${s.latest}; ${s.personal ? "your personal pins" : `${where}'s skilllib.json`} then pin${s.personal ? "" : "s"} it.`}`,
2938
3074
  at: rows(root, [s.name]),
2939
3075
  // It changes what the shared skilllib.json pins: a choice, never run by `doctor --fix`.
2940
- ...untrackable(s) ? {} : { choices: [{ label: `Install your newest (v${s.latest})`, hint: `${where}'s skilllib.json pins it`, run: () => `${s.name}: installed v${track(root, s.name).to}` }] }
3076
+ ...untrackable(s) ? {} : { choices: [{ label: `Install your newest (v${s.latest})`, hint: s.personal ? "your personal pins pin it" : `${where}'s skilllib.json pins it`, run: () => `${s.name}: installed v${track(root, s.name).to}` }] }
2941
3077
  });
2942
3078
  if (restorable.length > 0) {
2943
3079
  ctx.issues.push({
@@ -2957,7 +3093,7 @@ function repoIssues(root, ctx) {
2957
3093
  if (drifted.length) {
2958
3094
  const seen = drifted.some((s) => isShared(gitOf(root)?.of(relativeTo(root, s.path))));
2959
3095
  const yours = drifted.some((s) => untrackable(s));
2960
- const install = {
3096
+ const install2 = {
2961
3097
  label: "Install the pinned versions",
2962
3098
  hint: seen ? "git will see the copies change" : "your library keeps every version",
2963
3099
  run: () => summarize(where, "synced", syncProject(root, { names: drifted.map((s) => s.name) }))
@@ -2968,7 +3104,7 @@ function repoIssues(root, ctx) {
2968
3104
  title: `${where}: ${plural(drifted.length, "skill")} not at the pinned version`,
2969
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.` : ""}`,
2970
3106
  at: rows(root, drifted.map((s) => s.name)),
2971
- ...seen || yours ? { choices: [install] } : { fix: install }
3107
+ ...seen || yours ? { choices: [install2] } : { fix: install2 }
2972
3108
  });
2973
3109
  }
2974
3110
  for (const s of unstuck.filter((s2) => s2.state === "folder missing" && s2.latest === null))
@@ -2981,12 +3117,12 @@ function repoIssues(root, ctx) {
2981
3117
  // Editing skilllib.json is the team's call: a choice, never run by `doctor --fix`.
2982
3118
  choices: [
2983
3119
  {
2984
- label: "Remove it from skilllib.json",
3120
+ label: s.personal ? "Remove it from your personal pins" : "Remove it from skilllib.json",
2985
3121
  hint: `${where} stops pinning it`,
2986
3122
  run: () => {
2987
3123
  const r = removeSkill(root, s.name);
2988
3124
  if (r.action !== "removed") throw new Error(`${s.name}: ${r.reason}`);
2989
- return `${s.name}: removed from ${where}'s skilllib.json`;
3125
+ return `${s.name}: removed from ${pinsOf(where, s.personal)}`;
2990
3126
  }
2991
3127
  }
2992
3128
  ]
@@ -3003,7 +3139,7 @@ function repoIssues(root, ctx) {
3003
3139
  choices: [
3004
3140
  {
3005
3141
  label: "Update to the newest versions",
3006
- hint: isShared(gitOf(root)?.of(MANIFEST_FILE)) ? "git will see the new copies and skilllib.json" : "replaces each copy with your library's newest",
3142
+ hint: behind.some((s) => !s.personal) && isShared(gitOf(root)?.of(MANIFEST_FILE)) ? "git will see the new copies and skilllib.json" : "replaces each copy with your library's newest",
3007
3143
  run: () => summarize(where, "updated", updateProject(root, behind.map((s) => s.name)))
3008
3144
  }
3009
3145
  ]
@@ -3086,7 +3222,7 @@ function repoIssues(root, ctx) {
3086
3222
  if (partial.length) {
3087
3223
  const missing2 = [...new Set(partial.flatMap((s) => s.visibility.filter((v) => v.paths === 0).map((v) => v.id)))];
3088
3224
  const tracked = [...new Set(blindHere.flatMap(({ would }) => would.blocked))];
3089
- const manifestShared = partial.some((s) => s.managed) && isShared(gitOf(root)?.of(MANIFEST_FILE));
3225
+ const manifestShared = partial.some((s) => s.managed && !s.personal) && isShared(gitOf(root)?.of(MANIFEST_FILE));
3090
3226
  const run = (allowTracked) => () => {
3091
3227
  if (allowTracked) allowTrackedLinks(root);
3092
3228
  const r = linkAll(root, { allowTracked, names: partial.map((s) => s.name) });
@@ -3162,9 +3298,9 @@ function repoIssues(root, ctx) {
3162
3298
  id: `adopt:${root}:${s.name}`,
3163
3299
  severity: "suggestion",
3164
3300
  title: `${s.name} in ${where} isn't tracked`,
3165
- detail: "It's identical to the library copy; track it (in skilllib.json) so future library changes reach it.",
3301
+ detail: `It's identical to the library copy; track it (in ${newPinsOf(root, where)}) so future library changes reach it.`,
3166
3302
  at: rows(root, [s.name]),
3167
- choices: [{ label: "Track it", hint: `adds it to ${where}'s skilllib.json`, run: () => `${track(root, s.name).name} tracked in ${where}` }]
3303
+ choices: [{ label: "Track it", hint: `adds it to ${newPinsOf(root, where)}`, run: () => `${track(root, s.name).name} tracked in ${where}` }]
3168
3304
  });
3169
3305
  } else if (s.state === "untracked, differs from library" || s.state === "repo skill, differs from library") {
3170
3306
  const teams = s.state.startsWith("repo skill") || committed(s) || !!untrackable(s);
@@ -3226,7 +3362,7 @@ function repoIssues(root, ctx) {
3226
3362
  id: `not-in-lib:${root}:${s.name}`,
3227
3363
  severity: "suggestion",
3228
3364
  title: `${s.name} in ${where} is tracked, but no longer in your library`,
3229
- detail: `${where}'s skilllib.json pins it, but your library has no copy: if the folder goes, sync can't restore it, and your other repos can't add it. Copy ${where}'s copy back into the library.`,
3365
+ detail: `${s.personal ? "Your personal pins have" : `${where}'s skilllib.json pins`} it, but your library has no copy: if the folder goes, sync can't restore it, and your other repos can't add it. Copy ${where}'s copy back into the library.`,
3230
3366
  at: rows(root, [s.name]),
3231
3367
  choices: [
3232
3368
  {
@@ -3623,12 +3759,217 @@ function usesByProject(uses, projectOf) {
3623
3759
  return byProject;
3624
3760
  }
3625
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
+
3626
3967
  // src/move.ts
3627
- import { basename as basename9 } from "path";
3968
+ import { basename as basename10 } from "path";
3628
3969
  function moveGlobalSkills(skills, roots, {
3629
3970
  group,
3630
3971
  allowTracked = false,
3631
- label = basename9,
3972
+ label = basename10,
3632
3973
  allowTrackedHint = "re-run with --allow-tracked"
3633
3974
  } = {}) {
3634
3975
  const byName = /* @__PURE__ */ new Map();
@@ -3747,9 +4088,9 @@ function projectOfFactory(projects2) {
3747
4088
  const sorted2 = [...projects2].sort((a, b) => b.length - a.length);
3748
4089
  return (cwd) => {
3749
4090
  if (!cwd) return null;
3750
- const known = sorted2.find((p) => cwd === p || cwd.startsWith(p + sep9));
4091
+ const known = sorted2.find((p) => cwd === p || cwd.startsWith(p + sep10));
3751
4092
  if (known) return known;
3752
- const repo = cwd.split(`${sep9}.claude${sep9}worktrees${sep9}`)[0];
4093
+ const repo = cwd.split(`${sep10}.claude${sep10}worktrees${sep10}`)[0];
3753
4094
  return isProjectCandidate(repo) ? repo : null;
3754
4095
  };
3755
4096
  }
@@ -3763,7 +4104,7 @@ var projectsFor = (args) => {
3763
4104
  if (!args.all) return [requireProject()];
3764
4105
  const { readable, unreadable } = readableRoots(visibleProjects());
3765
4106
  for (const u of unreadable) {
3766
- error(`${basename10(u.root)}: ${u.reason}`);
4107
+ error(`${basename11(u.root)}: ${u.reason}`);
3767
4108
  process.exitCode = 1;
3768
4109
  }
3769
4110
  return readable;
@@ -3775,7 +4116,7 @@ function requireProject() {
3775
4116
  if (!isProjectCandidate(findProjectRoot())) {
3776
4117
  const top = findProjectRoot();
3777
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.`);
3778
- 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` : "";
3779
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.`);
3780
4121
  }
3781
4122
  throw new Error("Not in a git repo. Run this inside a project (or `git init` first).");
@@ -3797,7 +4138,7 @@ async function status(args) {
3797
4138
  const root = requireProject();
3798
4139
  const skills = projectStatus(root);
3799
4140
  const nested = nestedSkills(root);
3800
- info(`${basename10(root)} ${dim(tildify(root))}
4141
+ info(`${basename11(root)} ${dim(tildify(root))}
3801
4142
  `);
3802
4143
  if (skills.length === 0 && nested.length === 0) info("No skills in this project yet. Add one from your library with: skilllib add <name>");
3803
4144
  const usage2 = await usageBySkill(args.days, root);
@@ -3870,7 +4211,7 @@ async function list(args) {
3870
4211
  const installs = /* @__PURE__ */ new Map();
3871
4212
  for (const project of visibleProjects()) {
3872
4213
  for (const name of Object.keys(pinsIn(project))) {
3873
- installs.set(name, [...installs.get(name) ?? [], basename10(project)]);
4214
+ installs.set(name, [...installs.get(name) ?? [], basename11(project)]);
3874
4215
  }
3875
4216
  }
3876
4217
  const usage2 = await usageBySkill(args.days);
@@ -3913,7 +4254,7 @@ function teamStateIn(root, extraDirs = []) {
3913
4254
  function projects(args) {
3914
4255
  const { readable: roots, unreadable } = readableRoots(visibleProjects());
3915
4256
  for (const u of unreadable) {
3916
- error(`${basename10(u.root)}: ${u.reason}`);
4257
+ error(`${basename11(u.root)}: ${u.reason}`);
3917
4258
  process.exitCode = 1;
3918
4259
  }
3919
4260
  if (args.json) {
@@ -3922,7 +4263,7 @@ function projects(args) {
3922
4263
  roots.map((root) => {
3923
4264
  const teamState = teamStateIn(root);
3924
4265
  return {
3925
- name: basename10(root),
4266
+ name: basename11(root),
3926
4267
  path: root,
3927
4268
  skills: projectStatus(root).map((s) => ({ name: s.name, managed: s.managed, state: teamState(s), version: s.version, latest: s.latest }))
3928
4269
  };
@@ -3940,7 +4281,7 @@ function projects(args) {
3940
4281
  const usual = /* @__PURE__ */ new Set(["ok", "local only", "repo skill", "repo skill, in library", "from npx skills"]);
3941
4282
  const attention = skills.filter((s) => !usual.has(teamState(s))).length;
3942
4283
  return {
3943
- Project: basename10(root),
4284
+ Project: basename11(root),
3944
4285
  Path: dim(tildify(root)),
3945
4286
  Skills: skills.map((s) => s.name).join(", ") || dim("none"),
3946
4287
  "Local only": String(skills.filter((s) => teamState(s) === "local only").length),
@@ -3958,7 +4299,7 @@ function add(args) {
3958
4299
  printChanges(root, changes, args);
3959
4300
  const pins = readManifest(root).skills;
3960
4301
  const done = changes.filter((c) => c.action !== "skipped" && pins[c.name]);
3961
- if (!args.json && done.some((c) => pins[c.name].personal))
4302
+ if (!args.json && args.personal === void 0 && done.some((c) => c.action === "installed" && !c.moved && pins[c.name].personal))
3962
4303
  info(dim(`Pinned in your personal pins (${tildify(personalPinsDir())}) and kept out of git. --shared pins in skilllib.json for the team.`));
3963
4304
  rememberProjects([root]);
3964
4305
  }
@@ -3975,11 +4316,11 @@ function changeEach(args, change, nothing) {
3975
4316
  const changes = change(root);
3976
4317
  if (changes.some(incomplete)) process.exitCode = 1;
3977
4318
  if (!args.json) {
3978
- if (args.all) info(`${basename10(root)} ${dim(tildify(root))}`);
4319
+ if (args.all) info(`${basename11(root)} ${dim(tildify(root))}`);
3979
4320
  if (changes.length === 0) nothing(root);
3980
4321
  else printChanges(root, changes, args);
3981
4322
  }
3982
- return { project: basename10(root), path: root, changes: withDirs(root, changes) };
4323
+ return { project: basename11(root), path: root, changes: withDirs(root, changes) };
3983
4324
  });
3984
4325
  if (args.json) args.all ? json("changes --all", results) : json("changes", results[0].changes);
3985
4326
  return roots;
@@ -4010,7 +4351,7 @@ function update(args) {
4010
4351
  function outdated(args) {
4011
4352
  const roots = projectsFor(args);
4012
4353
  const behind = roots.flatMap(
4013
- (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 }))
4014
4355
  );
4015
4356
  if (args.json) return json("outdated", behind);
4016
4357
  const rows2 = behind.map((b) => ({ Project: b.project, Skill: b.skill, Installed: `v${b.installed}`, Latest: `v${b.latest}`, Status: colorState(b.state) }));
@@ -4021,7 +4362,7 @@ function outdated(args) {
4021
4362
  }
4022
4363
  function importSkills(args) {
4023
4364
  if (args.global && args.positional.length) throw new Error("skilllib import --global takes no folders: it imports all your global skills");
4024
- 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));
4025
4366
  if (dirs.length === 0) {
4026
4367
  throw new Error("Usage: skilllib import <skill-dir>... [--force] | skilllib import --global");
4027
4368
  }
@@ -4041,11 +4382,11 @@ function importSkills(args) {
4041
4382
  continue;
4042
4383
  }
4043
4384
  success(`${name} ${dim(result === "unchanged" ? "already in library" : result === "added" ? "added to library" : "library updated")}`);
4044
- const root = resolve8(dir, "..", "..", "..");
4385
+ const root = resolve9(dir, "..", "..", "..");
4045
4386
  const dep = projectHere(root) === root ? readManifest(root).skills[name] : void 0;
4046
- 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)) {
4047
4388
  const change = addSkill(root, name);
4048
- 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}`));
4049
4390
  }
4050
4391
  }
4051
4392
  if (args.json) json("import", results);
@@ -4061,14 +4402,14 @@ function scan(args) {
4061
4402
  const library = new Set(librarySkills().map((s) => s.name));
4062
4403
  const skillNames = (root) => [
4063
4404
  // Not a folder that links out of the repo: what's in it isn't the repo's (as in status).
4064
- .../* @__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)])
4065
4406
  ];
4066
4407
  const withSkills = found.filter((root) => skillNames(root).length > 0);
4067
4408
  info(`Scanned ${roots.map((r) => tildify(r)).join(", ")}: ${found.length} project${found.length === 1 ? "" : "s"}, ${withSkills.length} with skills
4068
4409
  `);
4069
4410
  table(
4070
4411
  withSkills.map((root) => ({
4071
- Project: basename10(root),
4412
+ Project: basename11(root),
4072
4413
  Path: dim(tildify(root)),
4073
4414
  Skills: skillNames(root).map((n) => library.has(n) ? n : yellow(n)).join(", ")
4074
4415
  }))
@@ -4125,7 +4466,7 @@ function newSkill(args) {
4125
4466
  if (!r.ok) {
4126
4467
  throw new Error(`Can't create ${name}: ${r.reason}`);
4127
4468
  }
4128
- success(`Created ${tildify(join15(r.dir, "SKILL.md"))}`);
4469
+ success(`Created ${tildify(join16(r.dir, "SKILL.md"))}`);
4129
4470
  }
4130
4471
  function restore(args) {
4131
4472
  const backups = [...listBackups(), ...pluginBackups()];
@@ -4238,7 +4579,7 @@ async function usage(args) {
4238
4579
  return json("usage", {
4239
4580
  usageDays: args.days,
4240
4581
  source: "Claude Code transcripts",
4241
- 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) })),
4242
4583
  unusedLibrarySkills: unused
4243
4584
  });
4244
4585
  }
@@ -4246,13 +4587,13 @@ async function usage(args) {
4246
4587
  info(`No skill usage in Claude Code transcripts from the last ${args.days} days.`);
4247
4588
  return;
4248
4589
  }
4249
- 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"))})`)}
4250
4591
  `);
4251
4592
  table(
4252
4593
  summaries.map((s) => ({
4253
4594
  Skill: s.skill,
4254
4595
  Uses: String(s.uses),
4255
- Projects: [...s.projects].map((p) => basename10(p)).join(", ") || dim("-"),
4596
+ Projects: [...s.projects].map((p) => basename11(p)).join(", ") || dim("-"),
4256
4597
  "Last used": lastUsed(s),
4257
4598
  Library: library.has(s.skill) ? green("yes") : dim("no")
4258
4599
  }))
@@ -4264,7 +4605,7 @@ async function usage(args) {
4264
4605
  }
4265
4606
  function show(args) {
4266
4607
  const name = args.positional[0];
4267
- const dir = name ? join15(libraryDir(), name) : "";
4608
+ const dir = name ? join16(libraryDir(), name) : "";
4268
4609
  if (!name || !isSkillDir(dir)) {
4269
4610
  throw new Error(name ? `${name} is not in the library` : "Usage: skilllib show <skill-name>");
4270
4611
  }
@@ -4276,8 +4617,8 @@ function show(args) {
4276
4617
  path: dir,
4277
4618
  latest: latestVersion(name)?.version ?? null,
4278
4619
  origin: libraryOrigins()[name] ?? "",
4279
- projects: projectsUsing(name).map((p) => basename10(p)),
4280
- 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")
4281
4622
  });
4282
4623
  }
4283
4624
  info(`${name} ${dim(tildify(dir))}`);
@@ -4299,7 +4640,7 @@ function harnessesCommand(args) {
4299
4640
  }
4300
4641
  const enabled = enabledHarnesses();
4301
4642
  for (const h of HARNESSES) {
4302
- 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" : ""}`)}`);
4303
4644
  }
4304
4645
  info(dim(`
4305
4646
  New installs go to ${installDirs(enabled).join(" (real copy) + ")}${installDirs(enabled).length > 1 ? " (link)" : ""}.`));
@@ -4308,7 +4649,7 @@ function link(args) {
4308
4649
  const roots = projectsFor(args);
4309
4650
  for (const root of roots) {
4310
4651
  const res = linkAll(root, { allowTracked: args.allowTracked });
4311
- const label = args.all ? `${basename10(root)}: ` : "";
4652
+ const label = args.all ? `${basename11(root)}: ` : "";
4312
4653
  if (res.linked.length) success(`${label}linked ${res.linked.length} skill(s) for your other agents`);
4313
4654
  else if (!args.all && !res.blocked.length && !res.uncommitted.length && !res.inTheWay.length) {
4314
4655
  const missing = projectStatus(root).filter((s) => s.state === "folder missing").map((s) => s.name);
@@ -4354,11 +4695,11 @@ ${unreviewed} not reviewed. Keep one global on purpose: skilllib global keep <na
4354
4695
  }
4355
4696
  function repoRoot(repo) {
4356
4697
  const looksLikePath = /[\\/]/.test(repo) || repo.startsWith(".") || repo.startsWith("~");
4357
- const named = looksLikePath ? [] : visibleProjects().filter((p) => basename10(p) === repo);
4698
+ const named = looksLikePath ? [] : visibleProjects().filter((p) => basename11(p) === repo);
4358
4699
  if (named.length === 1) return named[0];
4359
4700
  if (named.length > 1) throw new Error(`Several projects are named ${repo}: ${named.map(tildify).join(", ")}. Pass its path.`);
4360
4701
  const path = expandHome(repo);
4361
- 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)`);
4362
4703
  const root = projectHere(path);
4363
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`);
4364
4705
  return root;
@@ -4380,22 +4721,22 @@ function moveGlobal(args, names, yours) {
4380
4721
  moved,
4381
4722
  ...reason ? { reason } : {},
4382
4723
  ...left ? { left } : {},
4383
- 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] }))
4384
4725
  }))
4385
4726
  );
4386
4727
  }
4387
4728
  for (const m of moves) {
4388
- 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)`);
4389
4730
  else error(`${m.name}: ${m.reason}`);
4390
- 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)}`);
4391
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"}`);
4392
4733
  }
4393
- const toCommit = [...new Set(moves.filter((m) => m.moved).flatMap((m) => m.changes.map(([r]) => r)))].filter((r) => existsSync14(join15(r, MANIFEST_FILE)));
4394
- if (toCommit.length) info(dim(`Commit skilllib.json and the skill folders in ${toCommit.map((r) => basename10(r)).join(", ")}.`));
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)))];
4735
+ if (toCommit.length) info(dim(`Commit skilllib.json and the skill folders in ${toCommit.map((r) => basename11(r)).join(", ")}.`));
4395
4736
  }
4396
4737
  function tidy(args) {
4397
4738
  const names = args.positional;
4398
- 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)])) : {};
4399
4740
  if (args.keep && !names.length) {
4400
4741
  throw new Error("Usage: skilllib tidy <name>... --keep <folder>");
4401
4742
  }
@@ -4404,13 +4745,13 @@ function tidy(args) {
4404
4745
  for (const name of names) {
4405
4746
  const where = targets.map((root) => ({ root, ...foldersOf(root, name) })).filter((w) => w.all.length);
4406
4747
  if (!where.length) {
4407
- 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])}`}`);
4408
4749
  continue;
4409
4750
  }
4410
4751
  if (!args.keep) continue;
4411
4752
  for (const { root, real } of where) {
4412
4753
  if (real.includes(keepFor(root)[name])) continue;
4413
- const label = root ? `${basename10(root)} \xB7 ` : "";
4754
+ const label = root ? `${basename11(root)} \xB7 ` : "";
4414
4755
  problems.push(
4415
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"}`
4416
4757
  );
@@ -4420,7 +4761,7 @@ function tidy(args) {
4420
4761
  throw new Error(problems.join("\n"));
4421
4762
  }
4422
4763
  const reports = targets.map(
4423
- (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) }) }
4424
4765
  );
4425
4766
  let held = 0;
4426
4767
  let changed = 0;
@@ -4452,7 +4793,7 @@ function tidy(args) {
4452
4793
  }
4453
4794
  if (args.dryRun) info(dim("\nNothing changed (--dry-run)."));
4454
4795
  else if (changed) info(dim(`
4455
- 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.`));
4456
4797
  if (held) {
4457
4798
  process.exitCode = 1;
4458
4799
  info(dim(`${held} change${held === 1 ? "" : "s"} held back because git would see them. Re-run with --allow-git to make them.`));
@@ -4483,11 +4824,108 @@ function agentSkill(args) {
4483
4824
  else if (state === "outdated") warn("The skilllib skill is out of date. Run `skilllib agent-skill install` to update it.");
4484
4825
  else info("Your agents don't know about skilllib yet. Run `skilllib agent-skill install` so you can ask them about your skills.");
4485
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
+ }
4486
4923
 
4487
4924
  export {
4488
4925
  realPath,
4489
4926
  skilllibHome,
4490
4927
  PROJECT_SKILLS_DIR,
4928
+ FEATURED_HARNESSES,
4491
4929
  HARNESSES,
4492
4930
  harness,
4493
4931
  installDirs,
@@ -4497,11 +4935,14 @@ export {
4497
4935
  versionHistory,
4498
4936
  forgetLatest,
4499
4937
  latestVersion,
4938
+ versionForHash,
4500
4939
  projectHere,
4501
4940
  readManifest,
4941
+ personalPinsFile,
4502
4942
  knownProjects,
4503
4943
  rememberProjects,
4504
4944
  readableRoots,
4945
+ pinsIn,
4505
4946
  setJsonMode,
4506
4947
  isJsonMode,
4507
4948
  json,
@@ -4600,5 +5041,7 @@ export {
4600
5041
  link,
4601
5042
  globalCommand,
4602
5043
  tidy,
4603
- agentSkill
5044
+ agentSkill,
5045
+ search,
5046
+ install
4604
5047
  };