skilllib 1.1.0 → 1.2.1

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/dist/index.js CHANGED
@@ -19,10 +19,11 @@ import {
19
19
  show,
20
20
  status,
21
21
  sync,
22
+ tidy,
22
23
  unhide,
23
24
  update,
24
25
  usage
25
- } from "./chunk-HLCJQVHB.js";
26
+ } from "./chunk-O5TY4NWZ.js";
26
27
 
27
28
  // src/index.ts
28
29
  import { readFileSync } from "fs";
@@ -44,6 +45,9 @@ In a project:
44
45
  skilllib add <name>... Install library skills where your harnesses look
45
46
  skilllib remove <name>... Remove skills from this project
46
47
  skilllib link [--all] Make every skill here usable by all your agents (adds links)
48
+ skilllib tidy [--all|--global] One real copy per skill, plus only the links your agents need
49
+ (--dry-run to preview, --allow-git to change what git sees,
50
+ <name> --keep <folder> to pick the copy that wins)
47
51
  skilllib sync [--all] Install exactly the versions in skilllib.json
48
52
  skilllib outdated [--all] Skills with a newer version in the library
49
53
  skilllib update [name...] Move to the newest versions (--all: every project)
@@ -75,9 +79,9 @@ async function main() {
75
79
  const args = parseArgs(rest);
76
80
  switch (command) {
77
81
  case void 0:
78
- return process.stdin.isTTY && process.stdout.isTTY ? (await import("./tui-YF7BQ7CD.js")).tui() : status(args);
82
+ return process.stdin.isTTY && process.stdout.isTTY ? (await import("./tui-6BOQIKAU.js")).tui() : status(args);
79
83
  case "ui":
80
- return (await import("./tui-YF7BQ7CD.js")).tui();
84
+ return (await import("./tui-6BOQIKAU.js")).tui();
81
85
  case "status":
82
86
  return status(args);
83
87
  case "add":
@@ -106,6 +110,9 @@ async function main() {
106
110
  return projects();
107
111
  case "link":
108
112
  return link(args);
113
+ case "tidy":
114
+ case "dedup":
115
+ return tidy(args);
109
116
  case "harnesses":
110
117
  case "agents":
111
118
  return harnessesCommand(args);
@@ -1,7 +1,6 @@
1
1
  import {
2
2
  ALL_PROJECT_DIRS,
3
3
  HARNESSES,
4
- MANIFEST_FILE,
5
4
  addRoot,
6
5
  addSkill,
7
6
  allowTrackedLinks,
@@ -13,6 +12,7 @@ import {
13
12
  findIssues,
14
13
  findProjectRoot,
15
14
  forgetLatest,
15
+ gitInfo,
16
16
  harnessesChosen,
17
17
  importSkill,
18
18
  installDirs,
@@ -26,14 +26,18 @@ import {
26
26
  linkEverywhere,
27
27
  listBackups,
28
28
  machineSkills,
29
+ pluginBackups,
29
30
  projectOfFactory,
30
31
  projectStatus,
31
32
  readConfig,
32
33
  readSkillInfo,
34
+ relativeTo,
33
35
  relinkDependency,
34
36
  removeRoot,
35
37
  removeSkill,
36
38
  restoreBackup,
39
+ restorePlugin,
40
+ runFix,
37
41
  scanUsage,
38
42
  setHarnesses,
39
43
  setHidden,
@@ -48,7 +52,7 @@ import {
48
52
  versionHistory,
49
53
  visibilityOf,
50
54
  visibleProjects
51
- } from "./chunk-HLCJQVHB.js";
55
+ } from "./chunk-O5TY4NWZ.js";
52
56
 
53
57
  // src/tui/index.tsx
54
58
  import { render } from "ink";
@@ -56,7 +60,7 @@ import { render } from "ink";
56
60
  // src/tui/App.tsx
57
61
  import { spawn, spawnSync as spawnSync2 } from "child_process";
58
62
  import { homedir } from "os";
59
- import { basename as basename3, dirname, join as join4, sep } from "path";
63
+ import { basename as basename3, dirname, join as join3, sep } from "path";
60
64
  import { useEffect, useRef, useState as useState2 } from "react";
61
65
  import { Box as Box3, Text as Text3, useApp, useInput, useWindowSize } from "ink";
62
66
  import { lstatSync } from "fs";
@@ -104,6 +108,8 @@ function stateBadge(state) {
104
108
  return { icon: "\u2713", label: "in your skills too", color: color.green };
105
109
  case "repo skill, differs from library":
106
110
  return { icon: "\u2260", label: "differs from yours", color: color.yellow };
111
+ case "from npx skills":
112
+ return { icon: "\u2193", label: "npx skills", color: color.magenta };
107
113
  case "available":
108
114
  return { icon: " ", label: "", color: color.faint };
109
115
  }
@@ -262,7 +268,7 @@ function loadSnapshot() {
262
268
  library,
263
269
  machine,
264
270
  issues: findIssues(projects, machine, new Set(library.map((l) => l.name)), statusOf),
265
- backups: listBackups(),
271
+ backups: [...listBackups(), ...pluginBackups()].sort((a, b) => b.movedAt.localeCompare(a.movedAt)),
266
272
  roots: readConfig().roots,
267
273
  harnesses: enabledHarnesses(),
268
274
  harnessesChosen: harnessesChosen(),
@@ -284,6 +290,7 @@ function projectRows(root, library, status = projectStatus(root)) {
284
290
  location: s.location,
285
291
  path: s.path,
286
292
  visibility: s.visibility,
293
+ ...s.source ? { source: s.source } : {},
287
294
  description: library.find((l) => l.name === s.name)?.description ?? readSkillInfo(s.path).description
288
295
  })
289
296
  );
@@ -633,11 +640,10 @@ var HELP_TOPICS = [
633
640
  heading: "Badges",
634
641
  lines: [
635
642
  "\u273B \u2B21 \u25CE \u2124 Claude Code, Cursor, Codex, Zed \u2014 lit when that agent loads the skill here",
636
- "\xB2 Cursor reaches it through two folders (it reads several)",
637
643
  "\u26A0 loaded globally loads in every repo on this machine, and you haven't decided about it yet",
638
644
  "\u2713 global loads in every repo, and you keep it global on purpose (Enter \u2192 Keep global on purpose)",
639
- "\u29C9 N copies the same skill reaches agents from N places \u2014 keep one",
640
- "\u25CF in sync \xB7 \u2191 update \xB7 \u270E edited \xB7 \u25CB only here",
645
+ "\u29C9 N copies the same skill reaches agents from N places \u2014 Health has the fix",
646
+ "\u25CF in sync \xB7 \u2191 update \xB7 \u270E edited \xB7 \u25CB only here \xB7 \u2193 owner/repo installed here by npx skills",
641
647
  "\u2713 committed in git, so teammates get it \xB1 changed committed, with local edits",
642
648
  "+ not added not committed yet \u2205 ignored gitignored \u2014 only on your machine"
643
649
  ]
@@ -654,8 +660,9 @@ var HELP_TOPICS = [
654
660
  {
655
661
  heading: "In a repo (only agents working in that repo see them)",
656
662
  lines: [
657
- ".claude/skills Claude Code (Cursor reads it too). Where skilllib installs your skills.",
658
663
  ".agents/skills the shared folder: Codex, Cursor, Zed and most other agents. Repos often commit their team skills here.",
664
+ " skilllib (like `npx skills`) keeps the real copy here whenever one of your agents needs it.",
665
+ ".claude/skills Claude Code (Cursor reads it too). A link to .agents/skills, or the copy itself if only Claude Code needs it.",
659
666
  ".cursor/skills Cursor only. .codex/skills Cursor only (compatibility)."
660
667
  ]
661
668
  },
@@ -735,7 +742,7 @@ var HELP_TOPICS = [
735
742
  "Compat .claude/skills, .codex/skills, ~/.claude/skills, ~/.codex/skills",
736
743
  "Plus plugins from its marketplace, and Cursor's built-in skills",
737
744
  "Cloud Agents only see ~/.cursor/skills.",
738
- "Its docs don't say what happens when two folders hold the same skill (the \xB2 badge)."
745
+ "A skill in two folders is listed once (tested). A plugin's copy and yours are listed separately."
739
746
  ]
740
747
  },
741
748
  {
@@ -761,7 +768,7 @@ var HELP_TOPICS = [
761
768
  "User ~/.agents/skills",
762
769
  "Admin /etc/codex/skills",
763
770
  "System skills bundled by OpenAI",
764
- "It does NOT read .claude/skills \u2014 skilllib links your skills into .agents/skills when Codex is on."
771
+ "It does NOT read .claude/skills \u2014 skilllib keeps your skills' real copies in .agents/skills."
765
772
  ]
766
773
  },
767
774
  {
@@ -802,57 +809,18 @@ var HELP_TOPICS = [
802
809
  }
803
810
  ];
804
811
 
805
- // src/git.ts
806
- import { execFileSync } from "child_process";
807
- import { existsSync as existsSync2 } from "fs";
808
- import { join as join2, relative } from "path";
809
- function git(root, args, input) {
810
- try {
811
- return execFileSync("git", args, { cwd: root, encoding: "utf-8", input, stdio: ["pipe", "pipe", "ignore"], maxBuffer: 16 * 1024 * 1024 });
812
- } catch (err) {
813
- const out = err.stdout;
814
- return typeof out === "string" ? out : null;
815
- }
816
- }
817
- function gitInfo(root) {
818
- if (!existsSync2(join2(root, ".git"))) return null;
819
- const dirs = [...ALL_PROJECT_DIRS, MANIFEST_FILE].filter((d) => existsSync2(join2(root, d)));
820
- if (dirs.length === 0) return { of: () => "new" };
821
- const tracked = (git(root, ["ls-files", "-z", "--", ...dirs]) ?? "").split("\0").filter(Boolean);
822
- const dirty = (git(root, ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...dirs]) ?? "").split("\0").filter(Boolean).map((entry) => entry.slice(3));
823
- const ignoredCache = /* @__PURE__ */ new Map();
824
- const isIgnored = (rel) => {
825
- if (!ignoredCache.has(rel)) {
826
- const out = git(root, ["check-ignore", "--no-index", "--", rel]) ?? "";
827
- ignoredCache.set(rel, out.trim().length > 0);
828
- }
829
- return ignoredCache.get(rel);
830
- };
831
- const under = (list, rel) => list.some((p) => p === rel || p.startsWith(rel + "/"));
832
- return {
833
- of: (relPath) => {
834
- const rel = relPath.replace(/\/+$/, "");
835
- if (under(tracked, rel)) return under(dirty, rel) ? "changed" : "committed";
836
- return isIgnored(rel) ? "ignored" : "new";
837
- }
838
- };
839
- }
840
- function relativeTo(root, path) {
841
- return relative(root, path).split("\\").join("/");
842
- }
843
-
844
812
  // src/review.ts
845
813
  import { spawnSync } from "child_process";
846
- import { existsSync as existsSync3, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
847
- import { join as join3 } from "path";
814
+ import { existsSync as existsSync2, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
815
+ import { join as join2 } from "path";
848
816
  var EXCERPT_CHARS = 1200;
849
817
  function tildify(p) {
850
818
  const home = userHome();
851
819
  return p.startsWith(home) ? "~" + p.slice(home.length) : p;
852
820
  }
853
821
  function skillExcerpt(dir, chars) {
854
- const file = join3(dir, "SKILL.md");
855
- if (!existsSync3(file)) return { text: "(SKILL.md not found)", files: 0, size: 0 };
822
+ const file = join2(dir, "SKILL.md");
823
+ if (!existsSync2(file)) return { text: "(SKILL.md not found)", files: 0, size: 0 };
856
824
  const content = readFileSync2(file, "utf-8").replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "");
857
825
  let files = 0;
858
826
  try {
@@ -923,7 +891,7 @@ Reply with a short table (skill \xB7 decision \xB7 projects \xB7 one-line reason
923
891
  `;
924
892
  }
925
893
  function copyText(text) {
926
- const savedTo = join3(skilllibHome(), "review-prompt.md");
894
+ const savedTo = join2(skilllibHome(), "review-prompt.md");
927
895
  mkdirSync(skilllibHome(), { recursive: true });
928
896
  writeFileSync(savedTo, text);
929
897
  const tools = process.platform === "darwin" ? [["pbcopy", []]] : process.platform === "win32" ? [["clip", []]] : [
@@ -1063,15 +1031,18 @@ function versionText(r) {
1063
1031
  }
1064
1032
  var harnessName = (id) => HARNESSES.find((h) => h.id === id)?.name ?? id;
1065
1033
  function harnessChips(visibility, installed) {
1066
- return visibility.flatMap((v) => {
1034
+ return visibility.map((v) => {
1067
1035
  const h = HARNESSES.find((x) => x.id === v.id);
1068
1036
  const on = installed && v.paths > 0;
1069
- return [
1070
- { text: ` ${h?.icon ?? "?"}`, color: on ? h?.color : color.faint, dim: !on },
1071
- { text: v.paths > 1 && installed ? "\xB2" : " ", color: color.yellow }
1072
- ];
1037
+ return { text: ` ${h?.icon ?? "?"} `, color: on ? h?.color : color.faint, dim: !on };
1073
1038
  });
1074
1039
  }
1040
+ var BACKUP_LABEL = {
1041
+ trash: "deleted from library",
1042
+ "global-backup": "unloaded from global",
1043
+ "tidy-backup": "duplicate replaced by a link",
1044
+ plugin: "plugin removed"
1045
+ };
1075
1046
  var GIT_LABEL = {
1076
1047
  committed: { text: "\u2713 committed", color: color.green },
1077
1048
  changed: { text: "\xB1 changed", color: color.yellow },
@@ -1117,7 +1088,7 @@ function wrapAligned(line, width) {
1117
1088
  function hasLinks(root, name) {
1118
1089
  return ALL_PROJECT_DIRS.some((d) => {
1119
1090
  try {
1120
- return lstatSync(join4(root, d, name)).isSymbolicLink();
1091
+ return lstatSync(join3(root, d, name)).isSymbolicLink();
1121
1092
  } catch {
1122
1093
  return false;
1123
1094
  }
@@ -1282,8 +1253,8 @@ function App() {
1282
1253
  const pKey = placeKey(place);
1283
1254
  function projectSections(root) {
1284
1255
  const where = basename3(root);
1285
- const git2 = gitFor(root);
1286
- const gitOf = (path) => git2 ? git2.of(relativeTo(root, path)) : null;
1256
+ const git = gitFor(root);
1257
+ const gitOf = (path) => git ? git.of(relativeTo(root, path)) : null;
1287
1258
  const rows = snapshot.rows.get(root) ?? [];
1288
1259
  const projectActions = [
1289
1260
  {
@@ -1336,7 +1307,8 @@ function App() {
1336
1307
  const dir = r.path;
1337
1308
  const badge = stateBadge(r.state);
1338
1309
  const edited = r.state.startsWith("edited");
1339
- const inRepo = r.location === ".agents/skills" && !r.managed;
1310
+ const npx = r.state === "from npx skills";
1311
+ const inRepo = r.location === ".agents/skills" && !r.managed && !npx;
1340
1312
  const missing = r.visibility.filter((v) => v.paths === 0);
1341
1313
  const seenBy = r.visibility.filter((v) => v.paths > 0);
1342
1314
  const notInLibrary = r.state === "local only" || r.state === "untracked, differs from library" || r.state === "repo skill" || r.state === "repo skill, differs from library";
@@ -1377,7 +1349,10 @@ function App() {
1377
1349
  ...r.installed && missing.length ? [{ label: `Make visible to ${missing.map((v) => harnessName(v.id)).join(", ")}`, hint: "adds links; one real copy", run: () => makeVisible() }] : [],
1378
1350
  ...r.installed && !r.managed && r.visibility.some((v) => v.paths > 0) && hasLinks(root, r.name) ? [{ label: "Remove links", hint: "keeps the real folder", run: removeLinks }] : []
1379
1351
  ];
1380
- if (inRepo) {
1352
+ if (npx) {
1353
+ primary = { label: "copy to your skills", run: copyRepoSkillToLibrary };
1354
+ actions.push({ label: "Copy to Your skills", hint: "so other repos can use it; npx skills keeps this copy", run: copyRepoSkillToLibrary });
1355
+ } else if (inRepo) {
1381
1356
  const libraryAction = r.state === "repo skill" ? { label: "Copy to Your skills", hint: "so other repos can use it; this repo keeps its copy", run: copyRepoSkillToLibrary } : r.state === "repo skill, differs from library" ? { label: "Update Your skills from this repo's copy", hint: "becomes a new version", run: copyRepoSkillToLibrary } : { label: "Already in Your skills", hint: `v${r.latest}`, run: () => say(`${r.name} matches library v${r.latest}`) };
1382
1357
  primary = r.state === "repo skill, in library" ? missing.length ? { label: "make visible to all", run: () => makeVisible() } : void 0 : { label: "copy to your skills", run: copyRepoSkillToLibrary };
1383
1358
  actions.push(libraryAction, ...visibilityActions);
@@ -1437,7 +1412,7 @@ function App() {
1437
1412
  }
1438
1413
  if (!inRepo) actions.push(...visibilityActions);
1439
1414
  actions.push(
1440
- { label: "Edit SKILL.md", run: () => void edit(join4(dir, "SKILL.md")) },
1415
+ { label: "Edit SKILL.md", run: () => void edit(join3(dir, "SKILL.md")) },
1441
1416
  { label: "Open folder", run: () => openFolder(dir) },
1442
1417
  ...projectActions
1443
1418
  );
@@ -1446,11 +1421,11 @@ function App() {
1446
1421
  return {
1447
1422
  key,
1448
1423
  search: r.name,
1449
- group: prefixGroup(r.name),
1424
+ group: npx ? { key: `repo:${r.source}`, label: r.source, about: `Installed with \`npx skills add ${r.source}\` into this repo (recorded in skills-lock.json).` } : prefixGroup(r.name),
1450
1425
  skill: { name: r.name, path: dir },
1451
1426
  tags: [
1452
1427
  r.installed ? "in this project" : "installable",
1453
- ...r.installed && r.state !== "ok" && r.state !== "repo skill, in library" ? ["needs attention"] : []
1428
+ ...r.installed && r.state !== "ok" && r.state !== "repo skill, in library" && !npx ? ["needs attention"] : []
1454
1429
  ],
1455
1430
  row: {
1456
1431
  key,
@@ -1461,11 +1436,11 @@ function App() {
1461
1436
  },
1462
1437
  { text: r.name, grow: true, color: r.installed ? void 0 : color.muted },
1463
1438
  { text: whereLabel.padStart(8), width: 8, color: color.faint },
1464
- ...gitCell(r.installed ? gitOf(r.path) : null, !!git2),
1439
+ ...gitCell(r.installed ? gitOf(r.path) : null, !!git),
1465
1440
  ...harnessChips(r.visibility.length ? r.visibility : enabledIds.map((id) => ({ id, paths: 0 })), r.installed),
1466
1441
  { text: versionText(r).padStart(8), width: 8, color: behind ? color.yellow : color.muted },
1467
1442
  {
1468
- text: r.installed && dupes(r.name) > 1 ? ` \u29C9 ${dupes(r.name)} copies` : !r.installed && globalCopy ? " \u2197 already global" : ` ${badge.icon} ${badge.label}`,
1443
+ text: r.installed && dupes(r.name) > 1 ? ` \u29C9 ${dupes(r.name)} copies` : !r.installed && globalCopy ? " \u2197 already global" : ` ${badge.icon} ${npx ? r.source : badge.label}`,
1469
1444
  width: 19,
1470
1445
  color: r.installed && dupes(r.name) > 1 ? color.red : !r.installed && globalCopy ? color.yellow : badge.color
1471
1446
  },
@@ -1479,12 +1454,12 @@ function App() {
1479
1454
  dir,
1480
1455
  meta: [
1481
1456
  r.installed ? `${seenBy.length ? `loaded by ${seenBy.map((v) => `${harnessName(v.id)}${v.paths > 1 ? ` (via ${v.paths} folders)` : ""}`).join(", ")}` : "not loaded by your harnesses"}${missing.length ? ` \xB7 not ${missing.map((v) => harnessName(v.id)).join(", ")}` : ""}` : "not installed",
1482
- inRepo ? `committed in ${r.location}` : r.installed ? r.managed ? `v${r.version} installed in .claude/skills` : notInLibrary ? "only in this project" : "untracked copy" : "not installed",
1483
- ...r.installed && git2 ? [GIT_ABOUT[gitOf(r.path)]] : [],
1457
+ npx ? `installed by npx skills from ${r.source} into ${r.location}` : inRepo ? `committed in ${r.location}` : r.installed ? r.managed ? `v${r.version} installed in ${r.location}` : notInLibrary ? "only in this project" : "untracked copy" : "not installed",
1458
+ ...r.installed && git ? [GIT_ABOUT[gitOf(r.path)]] : [],
1484
1459
  r.inLibrary ? `in Your skills (v${r.latest}${origin ? `, from ${origin}` : ""})` : "not in Your skills",
1485
1460
  `${usesText(r.name, root)} Claude Code uses here (${USAGE_DAYS}d)`
1486
1461
  ],
1487
- ...r.installed && dupes(r.name) > 1 ? { description: `Loaded ${dupes(r.name)} times here: ${copiesOf(r.name).join(" \xB7 ")}. Agents may see it twice \u2014 keep one.` } : {}
1462
+ ...r.installed && dupes(r.name) > 1 ? { description: `Reaches your agents from ${dupes(r.name)} places: ${copiesOf(r.name).join(" \xB7 ")}. Health has the fix (your skill wins over plugins).` } : {}
1488
1463
  }
1489
1464
  };
1490
1465
  };
@@ -1535,7 +1510,7 @@ function App() {
1535
1510
  { text: "\u25C8 ", color: color.accentDim },
1536
1511
  { text: m.name, grow: true },
1537
1512
  { text: (m.kind === "global" || m.kind === "skills.sh" ? "global" : m.kind).padStart(8), width: 8, color: color.faint },
1538
- ...gitCell(null, !!git2),
1513
+ ...gitCell(null, !!git),
1539
1514
  ...harnessChips(enabledIds.map((id) => ({ id, paths: m.harnesses.includes(id) ? 1 : 0 })), true),
1540
1515
  { text: "", width: 8 },
1541
1516
  {
@@ -1556,7 +1531,7 @@ function App() {
1556
1531
  `${m.kind}: ${m.origin}`,
1557
1532
  kept ? "yours: kept global on purpose" : m.movable ? "yours: the cleanup wizard can scope it to projects" : "managed by the vendor"
1558
1533
  ],
1559
- description: dupes(m.name) > 1 ? `Loaded ${dupes(m.name)} times here: ${copiesOf(m.name).join(" \xB7 ")}. Agents may see it twice \u2014 keep one.` : sourceGroup(m)?.about
1534
+ description: dupes(m.name) > 1 ? `Reaches your agents from ${dupes(m.name)} places: ${copiesOf(m.name).join(" \xB7 ")}. Health has the fix (your skill wins over plugins).` : sourceGroup(m)?.about
1560
1535
  }
1561
1536
  };
1562
1537
  };
@@ -1601,12 +1576,23 @@ function App() {
1601
1576
  preview: {
1602
1577
  title: "Make every skill usable by all your agents",
1603
1578
  meta: [`${plural2(partial.length, "skill")} missing for ${missingAgents.map(harnessName).join(", ")}`],
1604
- description: `Adds relative symlinks so each agent's folder reaches the one real copy (e.g. .claude/skills/<name> \u2192 .agents/skills/<name>). The repo's files aren't changed; folders git tracks need your OK first. Cursor reads several folders, so it may see a linked skill through two paths (shown as \xB2).`
1579
+ description: `Adds relative symlinks so each agent's folder reaches the one real copy (e.g. .claude/skills/<name> \u2192 .agents/skills/<name>). The repo's files aren't changed; folders git tracks need your OK first. Cursor lists a skill it reaches through several folders once.`
1605
1580
  }
1606
1581
  }
1607
1582
  ] : [];
1608
1583
  const fromLibrary = local.filter((r) => r.managed || r.state === "untracked copy of library skill");
1609
- const repoOwn = local.filter((r) => !fromLibrary.includes(r));
1584
+ const fromNpx = local.filter((r) => r.state === "from npx skills");
1585
+ const repoOwn = local.filter((r) => !fromLibrary.includes(r) && !fromNpx.includes(r));
1586
+ const copyAllToLibrary = (members) => [
1587
+ {
1588
+ label: `Copy all ${members.length} into your library`,
1589
+ hint: "the repo keeps its copies",
1590
+ run: () => act(() => {
1591
+ const added = members.map((m) => importSkill(m.skill.path, { force: true })).filter((r) => r.status !== "unchanged").length;
1592
+ return ok(`${plural2(added, "skill")} copied into your library`);
1593
+ })
1594
+ }
1595
+ ];
1610
1596
  const yours = globals.filter((m) => m.movable);
1611
1597
  const keptYours = yours.filter((m) => snapshot.keptGlobal.has(m.name));
1612
1598
  const unreviewed = yours.filter((m) => !snapshot.keptGlobal.has(m.name));
@@ -1649,20 +1635,8 @@ function App() {
1649
1635
  }
1650
1636
  ]
1651
1637
  },
1652
- {
1653
- title: `The repo's own (${repoOwn.length})`,
1654
- items: repoOwn.map(item),
1655
- groupActions: (members) => [
1656
- {
1657
- label: `Copy all ${members.length} into your library`,
1658
- hint: "the repo keeps its copies",
1659
- run: () => act(() => {
1660
- const added = members.map((m) => importSkill(m.skill.path, { force: true })).filter((r) => r.status !== "unchanged").length;
1661
- return ok(`${plural2(added, "skill")} copied into your library`);
1662
- })
1663
- }
1664
- ]
1665
- },
1638
+ { title: `From npx skills (${fromNpx.length})`, items: fromNpx.map(item), groupActions: copyAllToLibrary },
1639
+ { title: `The repo's own (${repoOwn.length})`, items: repoOwn.map(item), groupActions: copyAllToLibrary },
1666
1640
  {
1667
1641
  title: `\u26A0 Global \xB7 yours, not reviewed, loaded in every repo (${unreviewed.length})`,
1668
1642
  items: [...cleanupItem, ...unreviewed.map(globalItem)],
@@ -1754,7 +1728,7 @@ function App() {
1754
1728
  primary: { label: "choose projects", run: toggleProjects.run },
1755
1729
  actions: [
1756
1730
  toggleProjects,
1757
- { label: "Edit SKILL.md", hint: "saving creates a new version", run: () => void edit(join4(librarySkillDir(l.name), "SKILL.md")) },
1731
+ { label: "Edit SKILL.md", hint: "saving creates a new version", run: () => void edit(join3(librarySkillDir(l.name), "SKILL.md")) },
1758
1732
  { label: "Open folder", run: () => openFolder(librarySkillDir(l.name)) },
1759
1733
  {
1760
1734
  label: "Copy review prompt",
@@ -2182,7 +2156,12 @@ function App() {
2182
2156
  title: "Issues",
2183
2157
  items: snapshot.issues.map((issue) => {
2184
2158
  const fix = issue.fix;
2185
- const run = fix ? () => confirm(`${fix.label}?`, () => ok(fix.run())) : void 0;
2159
+ const result = (c) => {
2160
+ const r = runFix(c);
2161
+ return r.ok ? ok(r.message) : warn(r.message);
2162
+ };
2163
+ const choices = (issue.choices ?? []).map((c) => ({ label: c.label, hint: c.hint, run: () => act(() => result(c)) }));
2164
+ const run = fix ? () => confirm(`${fix.label}?`, () => result(fix)) : choices.length ? () => setOverlay({ type: "menu", title: issue.title, index: 0, build: () => choices }) : void 0;
2186
2165
  return {
2187
2166
  key: issue.id,
2188
2167
  search: issue.title,
@@ -2196,10 +2175,10 @@ function App() {
2196
2175
  ]
2197
2176
  },
2198
2177
  primary: run ? { label: "fix", run } : void 0,
2199
- actions: run && fix ? [{ label: fix.label, run }] : [],
2178
+ actions: fix && run ? [{ label: fix.label, run }] : choices,
2200
2179
  preview: {
2201
2180
  title: issue.title,
2202
- meta: [issue.severity, fix ? `fix: ${fix.label.toLowerCase()}` : "no automatic fix"],
2181
+ meta: [issue.severity, fix ? `fix: ${fix.label.toLowerCase()}` : choices.length ? `${choices.length} ways to fix it: pick one` : "no automatic fix"],
2203
2182
  description: issue.detail
2204
2183
  }
2205
2184
  };
@@ -2209,7 +2188,7 @@ function App() {
2209
2188
  title: "Moved out \xB7 restorable",
2210
2189
  items: snapshot.backups.map((b) => {
2211
2190
  const restore = () => act(() => {
2212
- const r = restoreBackup(b);
2191
+ const r = b.kind === "plugin" ? restorePlugin(b) : restoreBackup(b);
2213
2192
  return r.ok ? ok(`${b.name} restored to ${tildify2(r.to)}`) : warn(`Can't restore ${b.name}: ${r.reason}`);
2214
2193
  });
2215
2194
  return {
@@ -2220,7 +2199,7 @@ function App() {
2220
2199
  key: b.path,
2221
2200
  cells: [
2222
2201
  { text: b.name, grow: true },
2223
- { text: b.kind === "trash" ? " deleted from library" : " unloaded from global", color: color.muted },
2202
+ { text: ` ${BACKUP_LABEL[b.kind]}`, color: color.muted },
2224
2203
  { text: ` ${b.movedAt}`, color: color.faint }
2225
2204
  ]
2226
2205
  },
@@ -2229,7 +2208,7 @@ function App() {
2229
2208
  preview: {
2230
2209
  title: b.name,
2231
2210
  dir: b.path,
2232
- meta: [b.kind === "trash" ? "deleted from the library" : "removed from ~/.claude/skills", `moved ${b.movedAt}`]
2211
+ meta: [b.kind === "plugin" ? "Claude Code plugin; restoring reinstalls it" : `from ${tildify2(b.from)}`, `moved ${b.movedAt}`]
2233
2212
  }
2234
2213
  };
2235
2214
  })
@@ -2280,7 +2259,7 @@ function App() {
2280
2259
  if (!r.ok) return say(`Can't create ${value}: ${r.reason}`, false);
2281
2260
  setSelected((s) => ({ ...s, "right:library": value.trim() }));
2282
2261
  reload();
2283
- void edit(join4(r.dir, "SKILL.md"));
2262
+ void edit(join3(r.dir, "SKILL.md"));
2284
2263
  }
2285
2264
  });
2286
2265
  }
@@ -2472,7 +2451,7 @@ function App() {
2472
2451
  "Skills your harnesses load in EVERY project (~/.claude/skills, ~/.agents/skills, plugins\u2026). Move yours into the library and add them only where needed."
2473
2452
  ],
2474
2453
  help: ["Help", "Where skills come from, and how Claude Code, Cursor, Codex and Zed find and use them. Press enter (or ? anywhere)."],
2475
- health: ["Health", "Broken links, skills loaded twice, out-of-date projects, repo-only skills \u2014 and everything skilllib moved out, ready to restore."],
2454
+ health: ["Health", "Broken links, duplicates, copies that differ, plugins that repeat your skills, skills some agents can't reach, out-of-date projects \u2014 and everything skilllib moved out, ready to restore."],
2476
2455
  settings: ["Settings", `Harnesses: ${snapshot.harnesses.map(harnessName).join(", ") || "none"} \xB7 Folders: ${snapshot.roots.map(tildify2).join(", ") || "none yet"}`]
2477
2456
  };
2478
2457
  const [title, description] = about[place.kind] ?? ["", ""];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skilllib",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
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 and Zed.",
5
5
  "keywords": [
6
6
  "agent-skills",