skilllib 1.11.4 → 1.11.5

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
@@ -2083,7 +2097,7 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
2083
2097
  if (git2 === void 0) git2 = gitInfo(root);
2084
2098
  const planned = installDirs(enabled).map(abs);
2085
2099
  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);
2100
+ 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
2101
  primary = [...reals].sort((a, b) => rank(a) - rank(b))[0];
2088
2102
  }
2089
2103
  if (!primary) continue;
@@ -2103,12 +2117,12 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
2103
2117
  const needed = wanted.has(e.dir);
2104
2118
  if (e.link) {
2105
2119
  if (e.real === primary.real) {
2106
- if (!needed && !dep && ["committed", "changed"].includes((git2 ??= gitInfo(root))?.of(rel(e.path)) ?? "")) continue;
2120
+ if (!needed && !dep && isShared((git2 ??= gitInfo(root))?.of(rel(e.path)))) continue;
2107
2121
  if (!needed) steps.push({ kind: "unlink", path: e.path });
2108
2122
  } else steps.push(needed ? { kind: "link", path: e.path, target: primary.path } : { kind: "unlink", path: e.path });
2109
2123
  } else {
2110
2124
  const differs = e.hash !== primary.hash ? { differs: true } : {};
2111
- if (!needed && ["committed", "changed"].includes((git2 ??= gitInfo(root))?.of(rel(e.path)) ?? "")) continue;
2125
+ if (!needed && !chosen && isShared((git2 ??= gitInfo(root))?.of(rel(e.path)))) continue;
2112
2126
  steps.push(needed ? { kind: "replace", path: e.path, target: primary.path, ...differs } : { kind: "remove", path: e.path, ...differs });
2113
2127
  }
2114
2128
  }
@@ -2148,13 +2162,13 @@ function planGlobalTidy({ keep = {}, enabled = enabledHarnesses() } = {}) {
2148
2162
  }
2149
2163
  function gitVisibleSteps(plan, git2 = plan.root ? gitInfo(plan.root) : null) {
2150
2164
  if (!plan.root || !git2) return [];
2151
- const shared = (rel) => ["committed", "changed"].includes(git2.of(rel));
2165
+ const shared = (rel) => isShared(git2.of(rel));
2152
2166
  const dep = readManifest(plan.root).skills[plan.name];
2153
2167
  const recordsLink = (s) => !!dep && (s.kind === "link" || s.kind === "replace") && !dep.links?.includes(relativeTo(plan.root, dirname5(s.path)));
2154
2168
  return plan.steps.filter((s) => shared(relativeTo(plan.root, s.path)) || recordsLink(s) && shared(MANIFEST_FILE));
2155
2169
  }
2156
- function applyTidy(plan, { git: git2 = "keep", info: info2 } = {}) {
2157
- const held = git2 === "keep" ? gitVisibleSteps(plan, info2) : [];
2170
+ function applyTidy(plan, { git: git2 = "keep" } = {}) {
2171
+ const held = git2 === "keep" ? gitVisibleSteps(plan) : [];
2158
2172
  const applied = [];
2159
2173
  for (const step of plan.steps) {
2160
2174
  if (held.includes(step) || !entryExists(step.path) || "target" in step && !entryExists(step.target)) continue;
@@ -2244,7 +2258,7 @@ var gitOf = (root) => {
2244
2258
  };
2245
2259
  var gitNote = (root, paths) => {
2246
2260
  const git2 = gitOf(root);
2247
- return paths.some((p) => ["committed", "changed"].includes(git2?.of(relativeTo(root, p)) ?? "")) ? "; git will see it" : "";
2261
+ return paths.some((p) => isShared(git2?.of(relativeTo(root, p)))) ? "; git will see it" : "";
2248
2262
  };
2249
2263
  var agentNames = (ids) => ids.map((id) => harness(id).name).join(", ");
2250
2264
  function conflictIssue(c) {
@@ -2265,6 +2279,7 @@ function conflictIssue(c) {
2265
2279
  at: rows(c.root, [c.name]),
2266
2280
  choices: winners.flatMap((x) => {
2267
2281
  const plan = planFor(x.dir);
2282
+ if (!plan?.steps.length) return [];
2268
2283
  const visible2 = plan ? gitVisibleSteps(plan).map((s) => describeStep(s, c.root)) : [];
2269
2284
  const label = `Keep the ${folderLabel(c.root, x.dir)} copy`;
2270
2285
  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 +2345,7 @@ function brokenLinksIn(root, opened) {
2330
2345
  root,
2331
2346
  folders2.map((f) => placeOf(f.real)).filter((p) => p !== "" && p !== ".." && !p.startsWith("../") && !isAbsolute3(p))
2332
2347
  );
2333
- const tracked = ["committed", "changed"].includes(git2?.of(placeOf(join13(real, name))) ?? "");
2348
+ const tracked = isShared(git2?.of(placeOf(join13(real, name))));
2334
2349
  const recorded = !!own && mends(own, relative4(own, real).split(sep7).join("/"), name);
2335
2350
  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
2351
  return [
@@ -2463,7 +2478,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
2463
2478
  const dupes = names.filter((n) => libraryNames.has(n) || loadedYours.has(n));
2464
2479
  if (!dupes.length) continue;
2465
2480
  const others = plugin.skills.length - dupes.length;
2466
- const pluginName = plugin.id.split("@")[0];
2481
+ const pluginName = pluginNameOf(plugin.id);
2467
2482
  const remove2 = {
2468
2483
  label: `Remove the plugin ${plugin.id}`,
2469
2484
  hint: plugin.extras.length ? `also removes its ${plugin.extras.join(", ")}` : "restorable with skilllib restore",
@@ -2520,7 +2535,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
2520
2535
  id: `plugin:${root}:${plugin.id}`,
2521
2536
  severity: "problem",
2522
2537
  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.`,
2538
+ 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
2539
  choices: repoChoices(plugin, root, inRepo),
2525
2540
  at: rows(root, dupes),
2526
2541
  plugin: plugin.id
@@ -2653,7 +2668,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
2653
2668
  }
2654
2669
  const drifted = unstuck.filter((s) => s.state === "out of sync" && knownPin(s));
2655
2670
  if (drifted.length) {
2656
- const seen = drifted.some((s) => ["committed", "changed"].includes(gitOf(root)?.of(relativeTo(root, s.path)) ?? ""));
2671
+ const seen = drifted.some((s) => isShared(gitOf(root)?.of(relativeTo(root, s.path))));
2657
2672
  const yours = drifted.some((s) => untrackable(s));
2658
2673
  const install = {
2659
2674
  label: "Install the pinned versions",
@@ -2701,7 +2716,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
2701
2716
  choices: [
2702
2717
  {
2703
2718
  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",
2719
+ 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
2720
  run: () => summarize(where, "updated", updateProject(root, behind.map((s) => s.name)))
2706
2721
  }
2707
2722
  ]
@@ -2784,7 +2799,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
2784
2799
  if (partial.length) {
2785
2800
  const missing2 = [...new Set(partial.flatMap((s) => s.visibility.filter((v) => v.paths === 0).map((v) => v.id)))];
2786
2801
  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) ?? "");
2802
+ const manifestShared = partial.some((s) => s.managed) && isShared(gitOf(root)?.of(MANIFEST_FILE));
2788
2803
  const run = (allowTracked) => () => {
2789
2804
  if (allowTracked) allowTrackedLinks(root);
2790
2805
  const r = linkAll(root, { allowTracked, names: partial.map((s) => s.name) });
@@ -2813,7 +2828,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
2813
2828
  }
2814
2829
  const differing = new Set(tidy2.conflicts.map((c) => c.name));
2815
2830
  const git2 = gitOf(root);
2816
- const committed = (s) => ["committed", "changed"].includes(git2?.of(relativeTo(root, s.path)) ?? "");
2831
+ const committed = (s) => isShared(git2?.of(relativeTo(root, s.path)));
2817
2832
  for (const s of unstuck) {
2818
2833
  if (isLink(s.path) || differing.has(s.name)) continue;
2819
2834
  if (s.state === "local only" && (committed(s) || untrackable(s))) {
@@ -2866,7 +2881,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
2866
2881
  });
2867
2882
  } else if (s.state === "untracked, differs from library" || s.state === "repo skill, differs from library") {
2868
2883
  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`;
2884
+ 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
2885
  issues.push({
2871
2886
  id: `differs:${root}:${s.name}`,
2872
2887
  severity: "suggestion",
@@ -3123,7 +3138,7 @@ function usageIssues(projects2, machine, libraryNames, uses, statusOf = projectS
3123
3138
  }
3124
3139
  if (uses.asUsed)
3125
3140
  for (const p of installedPlugins().filter((x) => x.agent === "claude-code" && x.on && x.skills.length)) {
3126
- const name = p.id.split("@")[0];
3141
+ const name = pluginNameOf(p.id);
3127
3142
  if (p.skills.some((path) => uses.asUsed(`${name}:${basename6(path)}`) > 0)) continue;
3128
3143
  issues.push({
3129
3144
  id: `plugin-unused:${installId(p)}`,
@@ -3203,7 +3218,7 @@ function usableHere(root, uses = /* @__PURE__ */ new Map()) {
3203
3218
  const loadedBy = s.visibility.filter((v) => v.paths > 0).map((v) => v.id);
3204
3219
  const behind = s.state.endsWith("update available");
3205
3220
  const gitState = git2?.of(relativeTo(root, s.path));
3206
- const repo = !s.managed && (s.state.startsWith("repo skill") || gitState === "committed" || gitState === "changed");
3221
+ const repo = !s.managed && (s.state.startsWith("repo skill") || isShared(gitState));
3207
3222
  const usual = s.state === "ok" || s.state === "local only" || repo && s.state !== "from npx skills";
3208
3223
  const attrs = {
3209
3224
  source: s.managed ? "library" : repo ? "repo" : "local",
@@ -3367,7 +3382,14 @@ function transcriptFiles(sinceMs) {
3367
3382
  return list2(base).flatMap((project) => {
3368
3383
  const dir = join14(base, project);
3369
3384
  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);
3385
+ return list2(dir).filter((f) => f.endsWith(".jsonl")).map((f) => join14(dir, f)).filter((f) => {
3386
+ try {
3387
+ const st = statSync5(f);
3388
+ return st.isFile() && st.mtimeMs >= sinceMs;
3389
+ } catch {
3390
+ return false;
3391
+ }
3392
+ });
3371
3393
  });
3372
3394
  }
3373
3395
  async function scanUsage(days) {
@@ -3430,7 +3452,7 @@ function moveGlobalSkills(skills, roots, {
3430
3452
  const elsewhere = blindWithoutGlobal(name, copies, knownProjects().filter((p) => !roots.some((r) => realPath(r) === realPath(p))));
3431
3453
  if (elsewhere.length) return { name, moved: false, reason: `nothing moved: ${blindSaid(name, elsewhere, label)}`, changes: [] };
3432
3454
  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)));
3455
+ const cant = roots.flatMap((root) => ((b) => b ? [`${label(root)} (${withoutForceHint(b)})`] : [])(trackBlocker(root, name) ?? addRefusal(root, name, hash)));
3434
3456
  if (cant.length) return { name, moved: false, reason: `nothing moved: it can't be added to ${cant.join(", ")}`, changes: [] };
3435
3457
  const imported = importSkill(copies[0].path).status;
3436
3458
  if (imported === "exists")
@@ -3438,7 +3460,7 @@ function moveGlobalSkills(skills, roots, {
3438
3460
  if (group) recordOrigin(name, `group: ${group}`);
3439
3461
  const changes = roots.map((root) => [root, addSkill(root, name, { allowTracked })]);
3440
3462
  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");
3463
+ const why = (reason = "") => withoutForceHint(reason) === reason ? reason : `${withoutForceHint(reason)}: move or remove it there first`;
3442
3464
  const kept = imported === "added" ? `; it's in Your skills now` : "";
3443
3465
  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
3466
  const blindIn = changes.flatMap(([root, c]) => ((b) => b.length ? [{ root, c, b }] : [])(blindWithoutGlobal(name, copies, [root])));
@@ -3471,6 +3493,7 @@ var FLAGS = {
3471
3493
  keep: { type: "string" },
3472
3494
  to: { type: "string", multiple: true }
3473
3495
  };
3496
+ var VALUE_FLAGS = Object.entries(FLAGS).flatMap(([name, f]) => f.type === "string" ? [`--${name}`] : []);
3474
3497
  function parseArgs(argv) {
3475
3498
  const { values: v, positionals } = parseFlags({ args: argv, options: FLAGS, allowPositionals: true, strict: true });
3476
3499
  const days = Number(v.days ?? USAGE_DAYS);
@@ -3688,7 +3711,7 @@ async function list(args) {
3688
3711
  }
3689
3712
  function teamStateIn(root, extraDirs = []) {
3690
3713
  const git2 = gitInfo(root, [...new Set(extraDirs)]);
3691
- const committed = (path) => ["committed", "changed"].includes(git2?.of(relativeTo(root, path)) ?? "");
3714
+ const committed = (path) => isShared(git2?.of(relativeTo(root, path)));
3692
3715
  const asRepo = {
3693
3716
  "local only": "repo skill",
3694
3717
  "untracked copy of library skill": "repo skill, in library",
@@ -4231,7 +4254,8 @@ function tidy(args) {
4231
4254
  for (const s of report.skipped.filter((x) => !names.length || names.includes(x.name))) info(dim(`- ${label} \xB7 ${s.name}: skipped, ${s.reason}`));
4232
4255
  }
4233
4256
  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."));
4257
+ else if (changed) info(dim(`
4258
+ Replaced copies are in ${tildify(join15(skilllibHome(), "tidy-backup"))}; \`skilllib restore\` puts them back.`));
4235
4259
  if (held) {
4236
4260
  process.exitCode = 1;
4237
4261
  info(dim(`${held} change${held === 1 ? "" : "s"} held back because git would see them. Re-run with --allow-git to make them.`));
@@ -4316,6 +4340,7 @@ export {
4316
4340
  projectStatus,
4317
4341
  linkEverywhere,
4318
4342
  relinkDependency,
4343
+ withoutForceHint,
4319
4344
  addRefusal,
4320
4345
  addSkill,
4321
4346
  removeSkill,
@@ -4329,6 +4354,7 @@ export {
4329
4354
  listBackups,
4330
4355
  movedTogether,
4331
4356
  restoreBackup,
4357
+ isShared,
4332
4358
  gitInfo,
4333
4359
  relativeTo,
4334
4360
  installedPlugins,
@@ -4351,6 +4377,7 @@ export {
4351
4377
  scanUsage,
4352
4378
  skillOf,
4353
4379
  moveGlobalSkills,
4380
+ VALUE_FLAGS,
4354
4381
  parseArgs,
4355
4382
  projectOfFactory,
4356
4383
  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-TE2EDZ7L.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-GFQUHPEC.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-GFQUHPEC.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-TE2EDZ7L.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",
@@ -2082,7 +2084,7 @@ function localSkill(root, s, git) {
2082
2084
  };
2083
2085
  const state = git ? git.of(relativeTo(root, s.path)) : null;
2084
2086
  const others = otherCopies(root, s.name, s.location);
2085
- const own = other[s.state]?.source === "untracked" && (state === "committed" || state === "changed") ? { source: "repo" } : {};
2087
+ const own = other[s.state]?.source === "untracked" && isShared(state) ? { source: "repo" } : {};
2086
2088
  return {
2087
2089
  name: s.name,
2088
2090
  source: "lib",
@@ -2247,6 +2249,7 @@ function said3(c, repo) {
2247
2249
  }
2248
2250
  function realOps(roots, backups, machine, libraryNames) {
2249
2251
  const rootOf = (repo) => roots.get(repo);
2252
+ const nameOf = (root) => [...roots].find(([, r]) => r === root)?.[0] ?? basename(root);
2250
2253
  const find = (repo, name) => projectStatus(rootOf(repo)).find((s) => s.name === name);
2251
2254
  const result = (c, repo) => c.action === "skipped" ? failed(said3(c, repo)) : said3(c, repo);
2252
2255
  const copyDir = (repo, name) => readManifest(rootOf(repo)).skills[name]?.dir ?? PROJECT_SKILLS_DIR;
@@ -2304,6 +2307,7 @@ function realOps(roots, backups, machine, libraryNames) {
2304
2307
  };
2305
2308
  const linkIn = (repo, name, allow) => {
2306
2309
  const s = find(repo, name);
2310
+ if (!s) throw new Error(`${name} isn't in ${repo} any more`);
2307
2311
  return s.managed ? relinkDependency(rootOf(repo), name, { allowTracked: allow }) : linkEverywhere(rootOf(repo), name, s.location, { allowTracked: allow });
2308
2312
  };
2309
2313
  const linkSaid = (repo, name, r) => {
@@ -2315,7 +2319,7 @@ function realOps(roots, backups, machine, libraryNames) {
2315
2319
  const orForce = (c, repo, label, force) => (
2316
2320
  // Whatever --force would get past (edits, a pin from another library, another copy in the way): offer it here.
2317
2321
  c.action === "skipped" && !!c.reason?.includes("--force") ? {
2318
- message: `${c.name} in ${repo}: ${c.reason.replace(/ \(use --force[^)]*\)$/, "")}`,
2322
+ message: `${c.name} in ${repo}: ${withoutForceHint(c.reason)}`,
2319
2323
  then: { label, preview: `What's there now in ${repo} goes to Settings \u203A Backups first.`, run: () => result(force(), repo) }
2320
2324
  } : result(c, repo)
2321
2325
  );
@@ -2329,7 +2333,7 @@ function realOps(roots, backups, machine, libraryNames) {
2329
2333
  remove: (repo, name) => {
2330
2334
  const s = find(repo, name);
2331
2335
  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)) ?? "");
2336
+ const committed = !s.managed && isShared(gitInfo(rootOf(repo))?.of(relativeTo(rootOf(repo), s.path)));
2333
2337
  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
2338
  const others = otherCopies(rootOf(repo), name, s.location);
2335
2339
  const left = (r) => typeof r === "string" && others.length ? `${r}; the copy in ${others.join(", ")} stays` : r;
@@ -2340,7 +2344,7 @@ function realOps(roots, backups, machine, libraryNames) {
2340
2344
  const c = updateProject(rootOf(repo), [name])[0];
2341
2345
  return c ? orForce(c, repo, "Update anyway", () => updateProject(rootOf(repo), [name], { force: true })[0]) : `${name} is up to date`;
2342
2346
  },
2343
- copyToLibrary: (repo, name) => toLibrary(find(repo, name).path),
2347
+ copyToLibrary: (repo, name) => ((s) => s ? toLibrary(s.path) : failed(`${name} isn't in ${repo} any more`))(find(repo, name)),
2344
2348
  deleteGlobal: (m) => {
2345
2349
  if (!entryExists(m.path)) return `${tildify(m.path)} is already gone`;
2346
2350
  const others = machine.filter((o) => o.name === m.name && o.path !== m.path && o.kind !== "plugin" && !o.broken);
@@ -2349,9 +2353,9 @@ function realOps(roots, backups, machine, libraryNames) {
2349
2353
  if (!m.broken) {
2350
2354
  const readers = [.../* @__PURE__ */ new Set([...m.agents, ...dependents.flatMap((o) => o.harnesses)])].filter((id) => !stays.some((o) => o.harnesses.includes(id)));
2351
2355
  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))}`);
2356
+ if (blind.length) return failed(`${m.name} kept: ${blindSaid(m.name, blind, nameOf)}`);
2353
2357
  }
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));
2358
+ const left = m.broken ? [] : strandedLinks(m.name, [{ path: m.path, links: [...m.links, ...dependents.map((o) => o.path)] }], [...roots.values(), ...knownProjects()], nameOf);
2355
2359
  const r = deleteGlobal(m.path, [...m.links, ...dependents.map((o) => o.path)]);
2356
2360
  if (!r.ok) return failed(`${m.name}: ${r.reason}`);
2357
2361
  if (m.broken) return `Removed the broken link ${m.name}`;
@@ -2361,7 +2365,7 @@ function realOps(roots, backups, machine, libraryNames) {
2361
2365
  keepGlobal: (name, keep) => (setKeepGlobal([name], keep), keep ? `${name} marked as global on purpose` : `${name} is no longer marked as global on purpose`),
2362
2366
  moveGlobal: (skills, repos, group) => {
2363
2367
  const repoRoots = repos.map(rootOf);
2364
- const label = (root) => repos[repoRoots.indexOf(root)] ?? [...roots].find(([, r]) => r === root)?.[0] ?? basename(root);
2368
+ const label = (root) => repos[repoRoots.indexOf(root)] ?? nameOf(root);
2365
2369
  const picked = new Set(skills.map((m) => m.name));
2366
2370
  const sourced = machine.filter((o) => picked.has(o.name) && o.movable && !o.broken);
2367
2371
  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 +2412,13 @@ function realOps(roots, backups, machine, libraryNames) {
2408
2412
  replacePlugin: (p, repos) => {
2409
2413
  const id = p.id;
2410
2414
  const clashes = p.skills.filter((s) => {
2411
- const mine = treeHash(librarySkillDir(s.name));
2412
- return mine !== null && mine !== treeHash(s.path);
2415
+ const theirs = treeHash(s.path);
2416
+ const mine = [librarySkillDir(s.name), ...machine.filter((m) => m.movable && m.name === s.name).map((m) => m.path)].map(treeHash);
2417
+ return mine.some((h) => h !== null && h !== theirs);
2413
2418
  });
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`);
2419
+ 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
2420
  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) ?? "")))
2421
+ (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
2422
  );
2418
2423
  if (cant.length) return failed(`${id} kept, nothing changed: ${cant.join("; ")}`);
2419
2424
  const imported = p.skills.map((s) => ({ name: s.name, status: importSkill(s.path).status }));
@@ -2464,7 +2469,7 @@ function realOps(roots, backups, machine, libraryNames) {
2464
2469
  if (s && isDir(s.path) && !notWritable(root, relative(root, dirname2(s.path)).split(sep).join("/"))) removeUntracked(root, name, s.path);
2465
2470
  const c = removeSkill(root, name, { force: true });
2466
2471
  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);
2472
+ const repo = nameOf(root);
2468
2473
  from.push(repo);
2469
2474
  if (c.reason) left.push(`in ${repo}, ${c.reason}`);
2470
2475
  }
@@ -2511,7 +2516,8 @@ function realOps(roots, backups, machine, libraryNames) {
2511
2516
  usageIssues: ({ usage, totals = {} }) => {
2512
2517
  const repoOf = new Map([...roots].map(([name, root]) => [root, name]));
2513
2518
  return usageIssues([...roots.values()], machine, libraryNames, {
2514
- inProject: (root, skill) => usage[repoOf.get(root)]?.[skill] ?? 0,
2519
+ // As doctor counts them: a plugin's copy (plugin:name, kept apart for the Plugins rows) counts for the skill.
2520
+ inProject: (root, skill) => Object.entries(usage[repoOf.get(root)] ?? {}).reduce((n, [key, count]) => n + (skillOf(key) === skill ? count : 0), 0),
2515
2521
  total: (skill) => totals[skill] ?? 0,
2516
2522
  asUsed: (key) => totals[key] ?? 0,
2517
2523
  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.5",
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",