skilllib 1.11.4 → 1.11.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -270,7 +270,7 @@ The same skill often reaches your agents more than once: a copy in `.claude/skil
270
270
  | Links no agent you use needs | Removes them. It keeps links that `skilllib.json` records, because teammates may use other agents. |
271
271
  | **Copies that differ** | Shows which agent runs which copy (e.g. Claude Code and Cursor run `.claude`, Codex runs `.agents`), and lets you pick the one to keep. It's your call, so **Fix all** never does it. |
272
272
  | Copies that differ behind a link (a link to another copy, in or out of the repo) | Reported only: skilllib never changes what a link points to. Make the copies the same, or remove the link. |
273
- | A Claude Code plugin with the same skill | Your skill wins. skilllib copies all the plugin's skills into Your skills, then removes the plugin or turns it off (`claude plugin uninstall` / `disable`). If the plugin also brings MCP servers, hooks, agents or commands, turning it off is the default. When the overlap is in one repo, you can instead turn the plugin off only there (in its `.claude/settings.local.json`: Claude Code makes git ignore it when it writes it; without the `claude` command, skilllib writes the file itself and makes git ignore it), or remove your copy from that repo. If the repo commits its copy, it's the team's: skilllib never offers to remove it. A plugin that a repo's own `.claude/settings.json` turns on counts only in that repo. Cursor ignores repo settings: after a repo turns the plugin off, Cursor still lists both there, and only turning the plugin off or removing it changes that. |
273
+ | A Claude Code plugin with the same skill | Your skill wins. skilllib copies the plugin's other skills into Your skills, then removes the plugin or turns it off (`claude plugin uninstall` / `disable`; a plugin only turned on in your settings, or synced from claude.ai, can only be turned off). If the plugin also brings MCP servers, hooks, agents or commands, turning it off is the default. When the overlap is in one repo, you can instead turn the plugin off only there (in its `.claude/settings.local.json`: Claude Code makes git ignore it when it writes it; without the `claude` command, skilllib writes the file itself and makes git ignore it), or remove your copy from that repo. If the repo commits its copy, it's the team's: skilllib never offers to remove it. A plugin that a repo's own `.claude/settings.json` turns on counts only in that repo. Cursor ignores repo settings: after a repo turns the plugin off, Cursor still lists both there, and only turning the plugin off or removing it changes that. |
274
274
  | A Cursor plugin with the same skill | Reported only. Turn the plugin off in Cursor. |
275
275
  | A repo copy of a skill you keep global | The repo copy is the extra one: remove it, or stop loading the skill globally after all. Claude Code runs the global copy anyway. |
276
276
  | A repo's own skill that also loads globally | Stop loading it globally (when no agent in any of your repos would then lack it). The repo's copy is the team's, so skilllib never offers to remove it. |
@@ -774,7 +774,7 @@ function isLink(path) {
774
774
  function notWritable(root, dir) {
775
775
  const top = realpathOrNull(root) ?? root;
776
776
  let p = root;
777
- for (const part of dir.split("/")) {
777
+ for (const part of dir.split(/[\\/]/)) {
778
778
  p = join8(p, part);
779
779
  if (!entryExists(p)) return null;
780
780
  const real = realpathOrNull(p);
@@ -1230,6 +1230,9 @@ function addPlan(root, name, hash, force) {
1230
1230
  }
1231
1231
  return { manifest, recorded, primary, wantedLinks, to, local };
1232
1232
  }
1233
+ function withoutForceHint(reason) {
1234
+ return reason.replace(/ \(use --force[^)]*\)$/, "");
1235
+ }
1233
1236
  function addRefusal(root, name, hash) {
1234
1237
  const plan = addPlan(root, name, hash, false);
1235
1238
  return "reason" in plan ? plan.reason : null;
@@ -1640,6 +1643,9 @@ import { basename as basename9, join as join15, resolve as resolve8, sep as sep8
1640
1643
  import { execFileSync as execFileSync2 } from "child_process";
1641
1644
  import { existsSync as existsSync9, realpathSync as realpathSync4 } from "fs";
1642
1645
  import { isAbsolute as isAbsolute2, join as join10, relative as relative3, sep as sep5 } from "path";
1646
+ function isShared(state) {
1647
+ return state === "committed" || state === "changed";
1648
+ }
1643
1649
  function git(root, args, input) {
1644
1650
  try {
1645
1651
  return execFileSync2("git", args, { cwd: root, encoding: "utf-8", input, stdio: ["pipe", "pipe", "ignore"], maxBuffer: 16 * 1024 * 1024 });
@@ -1708,6 +1714,9 @@ import { basename as basename6, dirname as dirname6, isAbsolute as isAbsolute3,
1708
1714
  import { execFileSync as execFileSync3 } from "child_process";
1709
1715
  import { existsSync as existsSync10, mkdirSync as mkdirSync6, readdirSync as readdirSync5, readFileSync as readFileSync6, statSync as statSync4, writeFileSync as writeFileSync4 } from "fs";
1710
1716
  import { basename as basename4, dirname as dirname4, join as join11, resolve as resolve7 } from "path";
1717
+ function pluginNameOf(id) {
1718
+ return id.split("@")[0];
1719
+ }
1711
1720
  function nonEmptyDir(path) {
1712
1721
  try {
1713
1722
  return readdirSync5(path).some((f) => !f.startsWith("."));
@@ -1898,6 +1907,10 @@ function turnOffIn(plugin, root) {
1898
1907
  const r = claude(["plugin", "disable", plugin.id, "--scope", "local"], root, bin);
1899
1908
  return r.ok ? { ok: true, message: done } : { ok: false, message: `${plugin.id}: ${r.reason}` };
1900
1909
  }
1910
+ function unplaced(p, verb) {
1911
+ if (!p.scope || p.scope === "user" || p.projectPath) return null;
1912
+ return { ok: false, message: `${p.id}: skilllib doesn't know which project it's installed in; run \`claude plugin ${verb} ${p.id} --scope ${p.scope}\` there` };
1913
+ }
1901
1914
  function settingsFileOf(p) {
1902
1915
  if (p.synced || !p.scope || p.scope === "user") return join11(claudeDir(), "settings.json");
1903
1916
  if (!p.projectPath) return null;
@@ -1909,7 +1922,8 @@ function whereOf(p) {
1909
1922
  function setPluginOn(p, on) {
1910
1923
  const shared = p.scope === "project" && p.projectPath ? "; it's in .claude/settings.json, which git tracks, so commit it if the team should get it" : "";
1911
1924
  const done = { ok: true, message: `${p.id} is ${on ? "on" : "off"}${whereOf(p)}${p.synced ? "" : "; Claude Code picks it up in its next session"}${shared}` };
1912
- if (p.scope && p.scope !== "user" && !p.projectPath) return { ok: false, message: `${p.id}: skilllib doesn't know which project it's installed in; run \`claude plugin ${on ? "enable" : "disable"} ${p.id} --scope ${p.scope}\` there` };
1925
+ const lost = unplaced(p, on ? "enable" : "disable");
1926
+ if (lost) return lost;
1913
1927
  const bin = p.synced || p.settingsOnly ? null : claudeBinary();
1914
1928
  if (bin) {
1915
1929
  const r2 = claude(["plugin", on ? "enable" : "disable", p.id, ...p.scope ? ["--scope", p.scope] : []], p.projectPath, bin);
@@ -1923,7 +1937,8 @@ function setPluginOn(p, on) {
1923
1937
  function uninstallPlugin(p) {
1924
1938
  if (p.synced) return { ok: false, message: `${p.id} is synced from your claude.ai account: remove it there, or turn it off here` };
1925
1939
  if (p.settingsOnly) return { ok: false, message: `${p.id} isn't installed, only turned on in your settings: turn it off instead` };
1926
- if (p.scope && p.scope !== "user" && !p.projectPath) return { ok: false, message: `${p.id}: skilllib doesn't know which project it's installed in; run \`claude plugin uninstall ${p.id} --scope ${p.scope}\` there` };
1940
+ const lost = unplaced(p, "uninstall");
1941
+ if (lost) return lost;
1927
1942
  const wasOn = enabledPlugins(p.projectPath)[p.id] === true;
1928
1943
  const r = claude(["plugin", "uninstall", p.id, "--keep-data", "--scope", p.scope ?? "user"], p.projectPath);
1929
1944
  if (!r.ok) return { ok: false, message: `${p.id}: ${r.reason}` };
@@ -1949,9 +1964,8 @@ function removedFile() {
1949
1964
  }
1950
1965
  function removePlugin(plugin, how) {
1951
1966
  if (plugin.settingsOnly && how === "delete") return { ok: false, message: `${plugin.id} isn't installed, only turned on in your settings: turn it off instead` };
1952
- if (plugin.scope && plugin.scope !== "user" && !plugin.projectPath) {
1953
- return { ok: false, message: `${plugin.id}: installed for one project; run \`claude plugin ${how === "delete" ? "uninstall" : "disable"} ${plugin.id} --scope ${plugin.scope}\` there` };
1954
- }
1967
+ const lost = unplaced(plugin, how === "delete" ? "uninstall" : "disable");
1968
+ if (lost) return lost;
1955
1969
  const viaSettings = plugin.synced || plugin.settingsOnly || !claudeBinary() && how === "off" && !plugin.projectPath;
1956
1970
  if (!viaSettings && !claudeBinary()) {
1957
1971
  return { ok: false, message: `${plugin.id}: nothing changed; Claude Code's CLI didn't run (tried \`claude\` on your PATH and ~/.local/bin/claude)` };
@@ -2003,7 +2017,7 @@ function restorePlugin(backup) {
2003
2017
  return { ok: true, to: where };
2004
2018
  }
2005
2019
  function cursorPluginSkills(plugins = cursorPlugins()) {
2006
- return plugins.flatMap((p) => p.skills.map((path) => ({ name: basename4(path), plugin: p.id.split("@")[0], path })));
2020
+ return plugins.flatMap((p) => p.skills.map((path) => ({ name: basename4(path), plugin: pluginNameOf(p.id), path })));
2007
2021
  }
2008
2022
 
2009
2023
  // src/tidy.ts
@@ -2020,6 +2034,9 @@ function entriesOf(dirs, name) {
2020
2034
  return [{ dir, path, link: link2, real, hash: null }];
2021
2035
  });
2022
2036
  }
2037
+ function uniqueReals(entries) {
2038
+ return entries.filter((e, i, all) => !e.link && all.findIndex((o) => !o.link && o.real === e.real) === i);
2039
+ }
2023
2040
  function hashReals(reals) {
2024
2041
  if (reals.length > 1) for (const r of reals) r.hash = treeHash(r.path);
2025
2042
  }
@@ -2060,15 +2077,16 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
2060
2077
  let git2;
2061
2078
  const skillsLock = projectSkillsLock(root);
2062
2079
  const dirs = repoTidyDirs(root).map(abs);
2080
+ const realRoot = realpathOrNull(root) ?? root;
2063
2081
  for (const name of namesIn(dirs)) {
2064
2082
  const entries = entriesOf(dirs, name);
2065
- const external = entries.find((e) => e.link && e.real && !(e.real + sep6).startsWith(realpathOrNull(root) + sep6));
2083
+ const external = entries.find((e) => e.link && e.real && !(e.real + sep6).startsWith(realRoot + sep6));
2066
2084
  if (external) {
2067
2085
  const differs = entries.some((e) => e !== external && e.real && existsSync11(join12(e.real, "SKILL.md")) && treeHash(e.real) !== treeHash(external.real));
2068
2086
  report.skipped.push({ name, reason: `${rel(external.path)} links outside the repo`, ...differs ? { differs } : {} });
2069
2087
  continue;
2070
2088
  }
2071
- const reals = entries.filter((e, i, all) => !e.link && all.findIndex((o) => !o.link && realpathOrNull(o.path) === realpathOrNull(e.path)) === i);
2089
+ const reals = uniqueReals(entries);
2072
2090
  if (reals.length === 0) continue;
2073
2091
  hashReals(reals);
2074
2092
  const dep = skills[name];
@@ -2083,7 +2101,7 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
2083
2101
  if (git2 === void 0) git2 = gitInfo(root);
2084
2102
  const planned = installDirs(enabled).map(abs);
2085
2103
  const fromSkillsSh = name in skillsLock;
2086
- const rank = (e) => (git2 && ["committed", "changed"].includes(git2.of(rel(e.path))) ? 0 : 1e3) + (fromSkillsSh && e.dir === abs(AGENTS_SKILLS_DIR) ? 0 : 100) + (planned.includes(e.dir) ? planned.indexOf(e.dir) : 50);
2104
+ const rank = (e) => (isShared(git2?.of(rel(e.path))) ? 0 : 1e3) + (fromSkillsSh && e.dir === abs(AGENTS_SKILLS_DIR) ? 0 : 100) + (planned.includes(e.dir) ? planned.indexOf(e.dir) : 50);
2087
2105
  primary = [...reals].sort((a, b) => rank(a) - rank(b))[0];
2088
2106
  }
2089
2107
  if (!primary) continue;
@@ -2103,12 +2121,12 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
2103
2121
  const needed = wanted.has(e.dir);
2104
2122
  if (e.link) {
2105
2123
  if (e.real === primary.real) {
2106
- if (!needed && !dep && ["committed", "changed"].includes((git2 ??= gitInfo(root))?.of(rel(e.path)) ?? "")) continue;
2124
+ if (!needed && !dep && isShared((git2 ??= gitInfo(root))?.of(rel(e.path)))) continue;
2107
2125
  if (!needed) steps.push({ kind: "unlink", path: e.path });
2108
2126
  } else steps.push(needed ? { kind: "link", path: e.path, target: primary.path } : { kind: "unlink", path: e.path });
2109
2127
  } else {
2110
2128
  const differs = e.hash !== primary.hash ? { differs: true } : {};
2111
- if (!needed && ["committed", "changed"].includes((git2 ??= gitInfo(root))?.of(rel(e.path)) ?? "")) continue;
2129
+ if (!needed && !chosen && isShared((git2 ??= gitInfo(root))?.of(rel(e.path)))) continue;
2112
2130
  steps.push(needed ? { kind: "replace", path: e.path, target: primary.path, ...differs } : { kind: "remove", path: e.path, ...differs });
2113
2131
  }
2114
2132
  }
@@ -2132,7 +2150,7 @@ function planGlobalTidy({ keep = {}, enabled = enabledHarnesses() } = {}) {
2132
2150
  report.skipped.push({ name, reason: "a Claude Code plugin kept in a skills folder" });
2133
2151
  continue;
2134
2152
  }
2135
- const reals = entries.filter((e, i, all) => !e.link && all.findIndex((o) => !o.link && realpathOrNull(o.path) === realpathOrNull(e.path)) === i);
2153
+ const reals = uniqueReals(entries);
2136
2154
  if (reals.length < 2) continue;
2137
2155
  hashReals(reals);
2138
2156
  const kept = reals.find((r) => r.dir === keep[name]);
@@ -2148,13 +2166,13 @@ function planGlobalTidy({ keep = {}, enabled = enabledHarnesses() } = {}) {
2148
2166
  }
2149
2167
  function gitVisibleSteps(plan, git2 = plan.root ? gitInfo(plan.root) : null) {
2150
2168
  if (!plan.root || !git2) return [];
2151
- const shared = (rel) => ["committed", "changed"].includes(git2.of(rel));
2169
+ const shared = (rel) => isShared(git2.of(rel));
2152
2170
  const dep = readManifest(plan.root).skills[plan.name];
2153
2171
  const recordsLink = (s) => !!dep && (s.kind === "link" || s.kind === "replace") && !dep.links?.includes(relativeTo(plan.root, dirname5(s.path)));
2154
2172
  return plan.steps.filter((s) => shared(relativeTo(plan.root, s.path)) || recordsLink(s) && shared(MANIFEST_FILE));
2155
2173
  }
2156
- function applyTidy(plan, { git: git2 = "keep", info: info2 } = {}) {
2157
- const held = git2 === "keep" ? gitVisibleSteps(plan, info2) : [];
2174
+ function applyTidy(plan, { git: git2 = "keep" } = {}) {
2175
+ const held = git2 === "keep" ? gitVisibleSteps(plan) : [];
2158
2176
  const applied = [];
2159
2177
  for (const step of plan.steps) {
2160
2178
  if (held.includes(step) || !entryExists(step.path) || "target" in step && !entryExists(step.target)) continue;
@@ -2244,7 +2262,7 @@ var gitOf = (root) => {
2244
2262
  };
2245
2263
  var gitNote = (root, paths) => {
2246
2264
  const git2 = gitOf(root);
2247
- return paths.some((p) => ["committed", "changed"].includes(git2?.of(relativeTo(root, p)) ?? "")) ? "; git will see it" : "";
2265
+ return paths.some((p) => isShared(git2?.of(relativeTo(root, p)))) ? "; git will see it" : "";
2248
2266
  };
2249
2267
  var agentNames = (ids) => ids.map((id) => harness(id).name).join(", ");
2250
2268
  function conflictIssue(c) {
@@ -2265,6 +2283,7 @@ function conflictIssue(c) {
2265
2283
  at: rows(c.root, [c.name]),
2266
2284
  choices: winners.flatMap((x) => {
2267
2285
  const plan = planFor(x.dir);
2286
+ if (!plan?.steps.length) return [];
2268
2287
  const visible2 = plan ? gitVisibleSteps(plan).map((s) => describeStep(s, c.root)) : [];
2269
2288
  const label = `Keep the ${folderLabel(c.root, x.dir)} copy`;
2270
2289
  if (!visible2.length) return [{ label, hint: x.dir === c.managed ? "skilllib's version" : "the others become links to it", run: keep(x.dir, "go") }];
@@ -2330,7 +2349,7 @@ function brokenLinksIn(root, opened) {
2330
2349
  root,
2331
2350
  folders2.map((f) => placeOf(f.real)).filter((p) => p !== "" && p !== ".." && !p.startsWith("../") && !isAbsolute3(p))
2332
2351
  );
2333
- const tracked = ["committed", "changed"].includes(git2?.of(placeOf(join13(real, name))) ?? "");
2352
+ const tracked = isShared(git2?.of(placeOf(join13(real, name))));
2334
2353
  const recorded = !!own && mends(own, relative4(own, real).split(sep7).join("/"), name);
2335
2354
  const pinnedThere = own ? ` ${placeOf(own)} has its own skilllib.json${recorded ? `, which records this link: skilllib sync in ${placeOf(own)} brings back what it points to` : ""}.` : "";
2336
2355
  return [
@@ -2463,7 +2482,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
2463
2482
  const dupes = names.filter((n) => libraryNames.has(n) || loadedYours.has(n));
2464
2483
  if (!dupes.length) continue;
2465
2484
  const others = plugin.skills.length - dupes.length;
2466
- const pluginName = plugin.id.split("@")[0];
2485
+ const pluginName = pluginNameOf(plugin.id);
2467
2486
  const remove2 = {
2468
2487
  label: `Remove the plugin ${plugin.id}`,
2469
2488
  hint: plugin.extras.length ? `also removes its ${plugin.extras.join(", ")}` : "restorable with skilllib restore",
@@ -2520,7 +2539,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
2520
2539
  id: `plugin:${root}:${plugin.id}`,
2521
2540
  severity: "problem",
2522
2541
  title: `${dupes.join(", ")}: also in the Claude Code plugin ${plugin.id}, on in ${where}`,
2523
- detail: `${where}'s Claude Code settings turn the plugin on, so Claude Code loads both there (the plugin's as /${plugin.id.split("@")[0]}:${dupes[0]}). Your skill wins.`,
2542
+ detail: `${where}'s Claude Code settings turn the plugin on, so Claude Code loads both there (the plugin's as /${pluginNameOf(plugin.id)}:${dupes[0]}). Your skill wins.`,
2524
2543
  choices: repoChoices(plugin, root, inRepo),
2525
2544
  at: rows(root, dupes),
2526
2545
  plugin: plugin.id
@@ -2653,7 +2672,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
2653
2672
  }
2654
2673
  const drifted = unstuck.filter((s) => s.state === "out of sync" && knownPin(s));
2655
2674
  if (drifted.length) {
2656
- const seen = drifted.some((s) => ["committed", "changed"].includes(gitOf(root)?.of(relativeTo(root, s.path)) ?? ""));
2675
+ const seen = drifted.some((s) => isShared(gitOf(root)?.of(relativeTo(root, s.path))));
2657
2676
  const yours = drifted.some((s) => untrackable(s));
2658
2677
  const install = {
2659
2678
  label: "Install the pinned versions",
@@ -2701,7 +2720,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
2701
2720
  choices: [
2702
2721
  {
2703
2722
  label: "Update to the newest versions",
2704
- hint: ["committed", "changed"].includes(gitOf(root)?.of(MANIFEST_FILE) ?? "") ? "git will see the new copies and skilllib.json" : "replaces each copy with your library's newest",
2723
+ hint: isShared(gitOf(root)?.of(MANIFEST_FILE)) ? "git will see the new copies and skilllib.json" : "replaces each copy with your library's newest",
2705
2724
  run: () => summarize(where, "updated", updateProject(root, behind.map((s) => s.name)))
2706
2725
  }
2707
2726
  ]
@@ -2784,7 +2803,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
2784
2803
  if (partial.length) {
2785
2804
  const missing2 = [...new Set(partial.flatMap((s) => s.visibility.filter((v) => v.paths === 0).map((v) => v.id)))];
2786
2805
  const tracked = [...new Set(blindHere.flatMap(({ would }) => would.blocked))];
2787
- const manifestShared = partial.some((s) => s.managed) && ["committed", "changed"].includes(gitOf(root)?.of(MANIFEST_FILE) ?? "");
2806
+ const manifestShared = partial.some((s) => s.managed) && isShared(gitOf(root)?.of(MANIFEST_FILE));
2788
2807
  const run = (allowTracked) => () => {
2789
2808
  if (allowTracked) allowTrackedLinks(root);
2790
2809
  const r = linkAll(root, { allowTracked, names: partial.map((s) => s.name) });
@@ -2813,7 +2832,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
2813
2832
  }
2814
2833
  const differing = new Set(tidy2.conflicts.map((c) => c.name));
2815
2834
  const git2 = gitOf(root);
2816
- const committed = (s) => ["committed", "changed"].includes(git2?.of(relativeTo(root, s.path)) ?? "");
2835
+ const committed = (s) => isShared(git2?.of(relativeTo(root, s.path)));
2817
2836
  for (const s of unstuck) {
2818
2837
  if (isLink(s.path) || differing.has(s.name)) continue;
2819
2838
  if (s.state === "local only" && (committed(s) || untrackable(s))) {
@@ -2866,7 +2885,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
2866
2885
  });
2867
2886
  } else if (s.state === "untracked, differs from library" || s.state === "repo skill, differs from library") {
2868
2887
  const teams = s.state.startsWith("repo skill") || committed(s) || !!untrackable(s);
2869
- const whose = s.state.startsWith("repo skill") || committed(s) ? "It's the team's copy (committed, or in the repo's own .agents/skills)" : `${untrackable(s)}, so skilllib can't track it here`;
2888
+ const whose = s.state.startsWith("repo skill") || committed(s) ? "It's the team's copy (committed, or in the repo's own .agents/skills or .grok/skills)" : `${untrackable(s)}, so skilllib can't track it here`;
2870
2889
  issues.push({
2871
2890
  id: `differs:${root}:${s.name}`,
2872
2891
  severity: "suggestion",
@@ -3123,7 +3142,7 @@ function usageIssues(projects2, machine, libraryNames, uses, statusOf = projectS
3123
3142
  }
3124
3143
  if (uses.asUsed)
3125
3144
  for (const p of installedPlugins().filter((x) => x.agent === "claude-code" && x.on && x.skills.length)) {
3126
- const name = p.id.split("@")[0];
3145
+ const name = pluginNameOf(p.id);
3127
3146
  if (p.skills.some((path) => uses.asUsed(`${name}:${basename6(path)}`) > 0)) continue;
3128
3147
  issues.push({
3129
3148
  id: `plugin-unused:${installId(p)}`,
@@ -3203,7 +3222,7 @@ function usableHere(root, uses = /* @__PURE__ */ new Map()) {
3203
3222
  const loadedBy = s.visibility.filter((v) => v.paths > 0).map((v) => v.id);
3204
3223
  const behind = s.state.endsWith("update available");
3205
3224
  const gitState = git2?.of(relativeTo(root, s.path));
3206
- const repo = !s.managed && (s.state.startsWith("repo skill") || gitState === "committed" || gitState === "changed");
3225
+ const repo = !s.managed && (s.state.startsWith("repo skill") || isShared(gitState));
3207
3226
  const usual = s.state === "ok" || s.state === "local only" || repo && s.state !== "from npx skills";
3208
3227
  const attrs = {
3209
3228
  source: s.managed ? "library" : repo ? "repo" : "local",
@@ -3367,7 +3386,14 @@ function transcriptFiles(sinceMs) {
3367
3386
  return list2(base).flatMap((project) => {
3368
3387
  const dir = join14(base, project);
3369
3388
  if (!statSync5(dir, { throwIfNoEntry: false })?.isDirectory()) return [];
3370
- return list2(dir).filter((f) => f.endsWith(".jsonl")).map((f) => join14(dir, f)).filter((f) => (statSync5(f, { throwIfNoEntry: false })?.mtimeMs ?? 0) >= sinceMs);
3389
+ return list2(dir).filter((f) => f.endsWith(".jsonl")).map((f) => join14(dir, f)).filter((f) => {
3390
+ try {
3391
+ const st = statSync5(f);
3392
+ return st.isFile() && st.mtimeMs >= sinceMs;
3393
+ } catch {
3394
+ return false;
3395
+ }
3396
+ });
3371
3397
  });
3372
3398
  }
3373
3399
  async function scanUsage(days) {
@@ -3430,7 +3456,7 @@ function moveGlobalSkills(skills, roots, {
3430
3456
  const elsewhere = blindWithoutGlobal(name, copies, knownProjects().filter((p) => !roots.some((r) => realPath(r) === realPath(p))));
3431
3457
  if (elsewhere.length) return { name, moved: false, reason: `nothing moved: ${blindSaid(name, elsewhere, label)}`, changes: [] };
3432
3458
  const hash = treeHash(copies[0].path) ?? "";
3433
- const cant = roots.flatMap((root) => ((b) => b ? [`${label(root)} (${b.replace(/ \(use --force to replace (it|the link)\)$/, "")})`] : [])(trackBlocker(root, name) ?? addRefusal(root, name, hash)));
3459
+ const cant = roots.flatMap((root) => ((b) => b ? [`${label(root)} (${withoutForceHint(b)})`] : [])(trackBlocker(root, name) ?? addRefusal(root, name, hash)));
3434
3460
  if (cant.length) return { name, moved: false, reason: `nothing moved: it can't be added to ${cant.join(", ")}`, changes: [] };
3435
3461
  const imported = importSkill(copies[0].path).status;
3436
3462
  if (imported === "exists")
@@ -3438,7 +3464,7 @@ function moveGlobalSkills(skills, roots, {
3438
3464
  if (group) recordOrigin(name, `group: ${group}`);
3439
3465
  const changes = roots.map((root) => [root, addSkill(root, name, { allowTracked })]);
3440
3466
  const skipped = changes.filter(([, c]) => c.action === "skipped");
3441
- const why = (reason = "") => reason.replace(/ \(use --force to replace it\)$/, ": move or remove it there first");
3467
+ const why = (reason = "") => withoutForceHint(reason) === reason ? reason : `${withoutForceHint(reason)}: move or remove it there first`;
3442
3468
  const kept = imported === "added" ? `; it's in Your skills now` : "";
3443
3469
  if (skipped.length) return { name, moved: false, reason: `not added to ${skipped.map(([r, c]) => `${label(r)} (${why(c.reason)})`).join(", ")}, so it still loads globally${kept}`, changes };
3444
3470
  const blindIn = changes.flatMap(([root, c]) => ((b) => b.length ? [{ root, c, b }] : [])(blindWithoutGlobal(name, copies, [root])));
@@ -3471,6 +3497,7 @@ var FLAGS = {
3471
3497
  keep: { type: "string" },
3472
3498
  to: { type: "string", multiple: true }
3473
3499
  };
3500
+ var VALUE_FLAGS = Object.entries(FLAGS).flatMap(([name, f]) => f.type === "string" ? [`--${name}`] : []);
3474
3501
  function parseArgs(argv) {
3475
3502
  const { values: v, positionals } = parseFlags({ args: argv, options: FLAGS, allowPositionals: true, strict: true });
3476
3503
  const days = Number(v.days ?? USAGE_DAYS);
@@ -3688,7 +3715,7 @@ async function list(args) {
3688
3715
  }
3689
3716
  function teamStateIn(root, extraDirs = []) {
3690
3717
  const git2 = gitInfo(root, [...new Set(extraDirs)]);
3691
- const committed = (path) => ["committed", "changed"].includes(git2?.of(relativeTo(root, path)) ?? "");
3718
+ const committed = (path) => isShared(git2?.of(relativeTo(root, path)));
3692
3719
  const asRepo = {
3693
3720
  "local only": "repo skill",
3694
3721
  "untracked copy of library skill": "repo skill, in library",
@@ -4231,7 +4258,8 @@ function tidy(args) {
4231
4258
  for (const s of report.skipped.filter((x) => !names.length || names.includes(x.name))) info(dim(`- ${label} \xB7 ${s.name}: skipped, ${s.reason}`));
4232
4259
  }
4233
4260
  if (args.dryRun) info(dim("\nNothing changed (--dry-run)."));
4234
- else if (changed) info(dim("\nReplaced copies are in ~/.skilllib/tidy-backup; `skilllib restore` puts them back."));
4261
+ else if (changed) info(dim(`
4262
+ Replaced copies are in ${tildify(join15(skilllibHome(), "tidy-backup"))}; \`skilllib restore\` puts them back.`));
4235
4263
  if (held) {
4236
4264
  process.exitCode = 1;
4237
4265
  info(dim(`${held} change${held === 1 ? "" : "s"} held back because git would see them. Re-run with --allow-git to make them.`));
@@ -4316,6 +4344,7 @@ export {
4316
4344
  projectStatus,
4317
4345
  linkEverywhere,
4318
4346
  relinkDependency,
4347
+ withoutForceHint,
4319
4348
  addRefusal,
4320
4349
  addSkill,
4321
4350
  removeSkill,
@@ -4329,6 +4358,7 @@ export {
4329
4358
  listBackups,
4330
4359
  movedTogether,
4331
4360
  restoreBackup,
4361
+ isShared,
4332
4362
  gitInfo,
4333
4363
  relativeTo,
4334
4364
  installedPlugins,
@@ -4351,6 +4381,7 @@ export {
4351
4381
  scanUsage,
4352
4382
  skillOf,
4353
4383
  moveGlobalSkills,
4384
+ VALUE_FLAGS,
4354
4385
  parseArgs,
4355
4386
  projectOfFactory,
4356
4387
  status,
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ VALUE_FLAGS,
3
4
  add,
4
5
  agentSkill,
5
6
  apply,
@@ -30,7 +31,7 @@ import {
30
31
  unhide,
31
32
  update,
32
33
  usage
33
- } from "./chunk-CS6T66EE.js";
34
+ } from "./chunk-TXQQQ73J.js";
34
35
 
35
36
  // src/index.ts
36
37
  import { readFileSync } from "fs";
@@ -125,7 +126,7 @@ async function main() {
125
126
  const argv = process.argv.slice(2);
126
127
  setJsonMode(argv.includes("--json"));
127
128
  let at = 0;
128
- while (argv[at]?.startsWith("--") && !["--version", "--help"].includes(argv[at])) at += ["--days", "--keep", "--to"].includes(argv[at]) ? 2 : 1;
129
+ while (argv[at]?.startsWith("--") && !["--version", "--help"].includes(argv[at])) at += VALUE_FLAGS.includes(argv[at]) ? 2 : 1;
129
130
  const command = argv[at];
130
131
  const rest = [...argv.slice(0, at), ...argv.slice(at + 1)];
131
132
  const args = parseArgs(rest);
@@ -137,10 +138,10 @@ async function main() {
137
138
  if (!["--version", "-v", "version", "help", "--help", "-h"].includes(command ?? "")) refreshAgentSkill();
138
139
  switch (command) {
139
140
  case void 0:
140
- return process.stdin.isTTY && process.stdout.isTTY ? (await import("./tui-BDLJOMSC.js")).tui() : overview(args);
141
+ return process.stdin.isTTY && process.stdout.isTTY ? (await import("./tui-OS2VUBGF.js")).tui() : overview(args);
141
142
  case "ui":
142
143
  if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("`skilllib ui` needs an interactive terminal. Agents and scripts: use `skilllib status --json`.");
143
- return (await import("./tui-BDLJOMSC.js")).tui();
144
+ return (await import("./tui-OS2VUBGF.js")).tui();
144
145
  case "status":
145
146
  return status(args);
146
147
  case "add":
@@ -29,6 +29,7 @@ import {
29
29
  installDirs,
30
30
  installedPlugins,
31
31
  isDir,
32
+ isShared,
32
33
  keptGlobal,
33
34
  knownProjects,
34
35
  latestVersion,
@@ -81,8 +82,9 @@ import {
81
82
  updateProject,
82
83
  usageIssues,
83
84
  versionHistory,
84
- visibleProjects
85
- } from "./chunk-CS6T66EE.js";
85
+ visibleProjects,
86
+ withoutForceHint
87
+ } from "./chunk-TXQQQ73J.js";
86
88
 
87
89
  // src/tui/index.tsx
88
90
  import { render } from "ink";
@@ -932,7 +934,7 @@ var reviewOption = (w, name, ui, repo) => ({
932
934
  label: "Review prompt",
933
935
  action: () => ui.copy(reviewPrompt(w, [name], `Review the skill ${name}${repo ? ` in ${repo}` : ""}.`, repo), `a review prompt for ${name}`)
934
936
  });
935
- var gitSees = (s) => s?.git === "committed" || s?.git === "changed" ? " Git will see it." : "";
937
+ var gitSees = (s) => isShared(s?.git) ? " Git will see it." : "";
936
938
  function versionsOption(w, repo, name, installed, ui) {
937
939
  return {
938
940
  label: "Other versions\u2026",
@@ -1741,7 +1743,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1741
1743
  if (isNameChar(input, key)) return setModal({ ...m, query: m.query + input.toLowerCase(), cursor: 0 });
1742
1744
  }
1743
1745
  }
1744
- const toastLines = toast && modal?.kind !== "confirm" ? wrap(toast, termCols - 2, 3).map((l) => " " + l) : [];
1746
+ const toastLines = toast && modal?.kind !== "confirm" ? wrap(toast, termCols - 2, Math.max(3, Math.floor(termRows / 3))).map((l) => " " + l) : [];
1745
1747
  const bottomH = Math.max(1, toastLines.length);
1746
1748
  const confirmLines = modal?.kind === "confirm" ? wrap(modal.fix.preview, columns - 6, 6) : [];
1747
1749
  const confirmH = modal?.kind === "confirm" ? confirmLines.length + 2 : 0;
@@ -1784,10 +1786,14 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1784
1786
  }), here: onHelp }
1785
1787
  ];
1786
1788
  const sideAt = sideRows.findIndex((r) => r.here);
1789
+ const brand = " \u25C6 skilllib ";
1790
+ const tabsWidth = (short) => PLACES.reduce((n, [p, label]) => n + (short && p === "library" ? 6 : label.length) + 2 + (p === "health" && healthCount ? ` ${healthCount}`.length : 0), 0);
1791
+ const showBrand = brand.length + tabsWidth(false) <= columns;
1792
+ const shortTabs = !showBrand && tabsWidth(false) > columns;
1787
1793
  const topBar = /* @__PURE__ */ jsx2(Box2, { height: 1, width: columns, justifyContent: "space-between", children: /* @__PURE__ */ jsxs2(Box2, { children: [
1788
- /* @__PURE__ */ jsx2(Text2, { color: color.accent, bold: true, children: " \u25C6 skilllib " }),
1794
+ /* @__PURE__ */ jsx2(Text2, { color: color.accent, bold: true, children: showBrand ? brand : "" }),
1789
1795
  PLACES.map(([p, label]) => /* @__PURE__ */ jsxs2(Text2, { backgroundColor: place === p ? color.accent : void 0, color: place === p ? "#0B1020" : color.muted, bold: place === p, children: [
1790
- ` ${label}`,
1796
+ ` ${shortTabs && p === "library" ? "Skills" : label}`,
1791
1797
  /* @__PURE__ */ jsx2(Text2, { color: place === p ? "#0B1020" : healthWorst ? SEV[healthWorst.severity].color : color.green, children: p === "health" && healthCount ? ` ${healthCount}` : "" }),
1792
1798
  " "
1793
1799
  ] }, p))
@@ -2082,7 +2088,7 @@ function localSkill(root, s, git) {
2082
2088
  };
2083
2089
  const state = git ? git.of(relativeTo(root, s.path)) : null;
2084
2090
  const others = otherCopies(root, s.name, s.location);
2085
- const own = other[s.state]?.source === "untracked" && (state === "committed" || state === "changed") ? { source: "repo" } : {};
2091
+ const own = other[s.state]?.source === "untracked" && isShared(state) ? { source: "repo" } : {};
2086
2092
  return {
2087
2093
  name: s.name,
2088
2094
  source: "lib",
@@ -2247,6 +2253,7 @@ function said3(c, repo) {
2247
2253
  }
2248
2254
  function realOps(roots, backups, machine, libraryNames) {
2249
2255
  const rootOf = (repo) => roots.get(repo);
2256
+ const nameOf = (root) => [...roots].find(([, r]) => r === root)?.[0] ?? basename(root);
2250
2257
  const find = (repo, name) => projectStatus(rootOf(repo)).find((s) => s.name === name);
2251
2258
  const result = (c, repo) => c.action === "skipped" ? failed(said3(c, repo)) : said3(c, repo);
2252
2259
  const copyDir = (repo, name) => readManifest(rootOf(repo)).skills[name]?.dir ?? PROJECT_SKILLS_DIR;
@@ -2304,6 +2311,7 @@ function realOps(roots, backups, machine, libraryNames) {
2304
2311
  };
2305
2312
  const linkIn = (repo, name, allow) => {
2306
2313
  const s = find(repo, name);
2314
+ if (!s) throw new Error(`${name} isn't in ${repo} any more`);
2307
2315
  return s.managed ? relinkDependency(rootOf(repo), name, { allowTracked: allow }) : linkEverywhere(rootOf(repo), name, s.location, { allowTracked: allow });
2308
2316
  };
2309
2317
  const linkSaid = (repo, name, r) => {
@@ -2315,7 +2323,7 @@ function realOps(roots, backups, machine, libraryNames) {
2315
2323
  const orForce = (c, repo, label, force) => (
2316
2324
  // Whatever --force would get past (edits, a pin from another library, another copy in the way): offer it here.
2317
2325
  c.action === "skipped" && !!c.reason?.includes("--force") ? {
2318
- message: `${c.name} in ${repo}: ${c.reason.replace(/ \(use --force[^)]*\)$/, "")}`,
2326
+ message: `${c.name} in ${repo}: ${withoutForceHint(c.reason)}`,
2319
2327
  then: { label, preview: `What's there now in ${repo} goes to Settings \u203A Backups first.`, run: () => result(force(), repo) }
2320
2328
  } : result(c, repo)
2321
2329
  );
@@ -2329,7 +2337,7 @@ function realOps(roots, backups, machine, libraryNames) {
2329
2337
  remove: (repo, name) => {
2330
2338
  const s = find(repo, name);
2331
2339
  if (!s) return failed(`${name} isn't in ${repo}`);
2332
- const committed = !s.managed && ["committed", "changed"].includes(gitInfo(rootOf(repo))?.of(relativeTo(rootOf(repo), s.path)) ?? "");
2340
+ const committed = !s.managed && isShared(gitInfo(rootOf(repo))?.of(relativeTo(rootOf(repo), s.path)));
2333
2341
  if (s.state.startsWith("repo skill") || committed) return failed(`${name} is the repo's own (committed by your team): skilllib doesn't delete it`);
2334
2342
  const others = otherCopies(rootOf(repo), name, s.location);
2335
2343
  const left = (r) => typeof r === "string" && others.length ? `${r}; the copy in ${others.join(", ")} stays` : r;
@@ -2340,7 +2348,7 @@ function realOps(roots, backups, machine, libraryNames) {
2340
2348
  const c = updateProject(rootOf(repo), [name])[0];
2341
2349
  return c ? orForce(c, repo, "Update anyway", () => updateProject(rootOf(repo), [name], { force: true })[0]) : `${name} is up to date`;
2342
2350
  },
2343
- copyToLibrary: (repo, name) => toLibrary(find(repo, name).path),
2351
+ copyToLibrary: (repo, name) => ((s) => s ? toLibrary(s.path) : failed(`${name} isn't in ${repo} any more`))(find(repo, name)),
2344
2352
  deleteGlobal: (m) => {
2345
2353
  if (!entryExists(m.path)) return `${tildify(m.path)} is already gone`;
2346
2354
  const others = machine.filter((o) => o.name === m.name && o.path !== m.path && o.kind !== "plugin" && !o.broken);
@@ -2349,9 +2357,9 @@ function realOps(roots, backups, machine, libraryNames) {
2349
2357
  if (!m.broken) {
2350
2358
  const readers = [.../* @__PURE__ */ new Set([...m.agents, ...dependents.flatMap((o) => o.harnesses)])].filter((id) => !stays.some((o) => o.harnesses.includes(id)));
2351
2359
  const blind = blindWithoutGlobal(m.name, [{ path: m.path, harnesses: readers, links: [...m.links, ...dependents.map((o) => o.path)] }], [...roots.values(), ...knownProjects()]);
2352
- if (blind.length) return failed(`${m.name} kept: ${blindSaid(m.name, blind, (root) => [...roots].find(([, r2]) => r2 === root)?.[0] ?? basename(root))}`);
2360
+ if (blind.length) return failed(`${m.name} kept: ${blindSaid(m.name, blind, nameOf)}`);
2353
2361
  }
2354
- const left = m.broken ? [] : strandedLinks(m.name, [{ path: m.path, links: [...m.links, ...dependents.map((o) => o.path)] }], [...roots.values(), ...knownProjects()], (root) => [...roots].find(([, r2]) => r2 === root)?.[0] ?? basename(root));
2362
+ const left = m.broken ? [] : strandedLinks(m.name, [{ path: m.path, links: [...m.links, ...dependents.map((o) => o.path)] }], [...roots.values(), ...knownProjects()], nameOf);
2355
2363
  const r = deleteGlobal(m.path, [...m.links, ...dependents.map((o) => o.path)]);
2356
2364
  if (!r.ok) return failed(`${m.name}: ${r.reason}`);
2357
2365
  if (m.broken) return `Removed the broken link ${m.name}`;
@@ -2361,7 +2369,7 @@ function realOps(roots, backups, machine, libraryNames) {
2361
2369
  keepGlobal: (name, keep) => (setKeepGlobal([name], keep), keep ? `${name} marked as global on purpose` : `${name} is no longer marked as global on purpose`),
2362
2370
  moveGlobal: (skills, repos, group) => {
2363
2371
  const repoRoots = repos.map(rootOf);
2364
- const label = (root) => repos[repoRoots.indexOf(root)] ?? [...roots].find(([, r]) => r === root)?.[0] ?? basename(root);
2372
+ const label = (root) => repos[repoRoots.indexOf(root)] ?? nameOf(root);
2365
2373
  const picked = new Set(skills.map((m) => m.name));
2366
2374
  const sourced = machine.filter((o) => picked.has(o.name) && o.movable && !o.broken);
2367
2375
  const attempt = (names) => moveGlobalSkills(names ? sourced.filter((m) => names.includes(m.name)) : sourced, repoRoots, { group, label, allowTrackedHint: "allow links there, then move it again" });
@@ -2408,12 +2416,13 @@ function realOps(roots, backups, machine, libraryNames) {
2408
2416
  replacePlugin: (p, repos) => {
2409
2417
  const id = p.id;
2410
2418
  const clashes = p.skills.filter((s) => {
2411
- const mine = treeHash(librarySkillDir(s.name));
2412
- return mine !== null && mine !== treeHash(s.path);
2419
+ const theirs = treeHash(s.path);
2420
+ const mine = [librarySkillDir(s.name), ...machine.filter((m) => m.movable && m.name === s.name).map((m) => m.path)].map(treeHash);
2421
+ return mine.some((h) => h !== null && h !== theirs);
2413
2422
  });
2414
- if (clashes.length) return failed(`${id} kept: your library has different ${clashes.map((s) => s.name).join(", ")}; update or delete ${clashes.length === 1 ? "it" : "them"} first`);
2423
+ if (clashes.length) return failed(`${id} kept: you have different ${clashes.map((s) => s.name).join(", ")} (in your library or global skills); keep yours, or update or delete ${clashes.length === 1 ? "it" : "them"} first`);
2415
2424
  const cant = repos.flatMap(
2416
- (repo) => p.skills.flatMap((s) => ((why) => why ? [`${s.name} in ${repo}: ${why.replace(/ \(use --force[^)]*\)$/, "")}`] : [])(trackBlocker(rootOf(repo), s.name) ?? addRefusal(rootOf(repo), s.name, treeHash(s.path) ?? "")))
2425
+ (repo) => p.skills.flatMap((s) => ((why) => why ? [`${s.name} in ${repo}: ${withoutForceHint(why)}`] : [])(trackBlocker(rootOf(repo), s.name) ?? addRefusal(rootOf(repo), s.name, treeHash(s.path) ?? "")))
2417
2426
  );
2418
2427
  if (cant.length) return failed(`${id} kept, nothing changed: ${cant.join("; ")}`);
2419
2428
  const imported = p.skills.map((s) => ({ name: s.name, status: importSkill(s.path).status }));
@@ -2464,7 +2473,7 @@ function realOps(roots, backups, machine, libraryNames) {
2464
2473
  if (s && isDir(s.path) && !notWritable(root, relative(root, dirname2(s.path)).split(sep).join("/"))) removeUntracked(root, name, s.path);
2465
2474
  const c = removeSkill(root, name, { force: true });
2466
2475
  if (c.action === "skipped") return failed(`${name}: ${c.reason}${from.length ? `; removed from ${from.join(", ")} so far` : ""}`);
2467
- const repo = [...roots].find(([, r2]) => r2 === root)?.[0] ?? basename(root);
2476
+ const repo = nameOf(root);
2468
2477
  from.push(repo);
2469
2478
  if (c.reason) left.push(`in ${repo}, ${c.reason}`);
2470
2479
  }
@@ -2511,7 +2520,8 @@ function realOps(roots, backups, machine, libraryNames) {
2511
2520
  usageIssues: ({ usage, totals = {} }) => {
2512
2521
  const repoOf = new Map([...roots].map(([name, root]) => [root, name]));
2513
2522
  return usageIssues([...roots.values()], machine, libraryNames, {
2514
- inProject: (root, skill) => usage[repoOf.get(root)]?.[skill] ?? 0,
2523
+ // As doctor counts them: a plugin's copy (plugin:name, kept apart for the Plugins rows) counts for the skill.
2524
+ inProject: (root, skill) => Object.entries(usage[repoOf.get(root)] ?? {}).reduce((n, [key, count]) => n + (skillOf(key) === skill ? count : 0), 0),
2515
2525
  total: (skill) => totals[skill] ?? 0,
2516
2526
  asUsed: (key) => totals[key] ?? 0,
2517
2527
  days: 30
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skilllib",
3
- "version": "1.11.4",
3
+ "version": "1.11.6",
4
4
  "description": "Manage your agent skills like dependencies. One library for all your skills; each repo gets only the ones it needs, and every agent can see them. Works with Claude Code, Cursor, Codex, Zed and Grok.",
5
5
  "keywords": [
6
6
  "agent-skills",