skilllib 1.0.2 → 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.
@@ -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,10 +12,12 @@ import {
13
12
  findIssues,
14
13
  findProjectRoot,
15
14
  forgetLatest,
15
+ gitInfo,
16
16
  harnessesChosen,
17
17
  importSkill,
18
18
  installDirs,
19
19
  isProjectCandidate,
20
+ keptGlobal,
20
21
  latestVersion,
21
22
  libraryOrigins,
22
23
  librarySkillDir,
@@ -25,17 +26,22 @@ import {
25
26
  linkEverywhere,
26
27
  listBackups,
27
28
  machineSkills,
29
+ pluginBackups,
28
30
  projectOfFactory,
29
31
  projectStatus,
30
32
  readConfig,
31
33
  readSkillInfo,
34
+ relativeTo,
32
35
  relinkDependency,
33
36
  removeRoot,
34
37
  removeSkill,
35
38
  restoreBackup,
39
+ restorePlugin,
40
+ runFix,
36
41
  scanUsage,
37
42
  setHarnesses,
38
43
  setHidden,
44
+ setKeepGlobal,
39
45
  skilllibHome,
40
46
  summarize,
41
47
  syncProject,
@@ -46,7 +52,7 @@ import {
46
52
  versionHistory,
47
53
  visibilityOf,
48
54
  visibleProjects
49
- } from "./chunk-67BDL5SN.js";
55
+ } from "./chunk-O5TY4NWZ.js";
50
56
 
51
57
  // src/tui/index.tsx
52
58
  import { render } from "ink";
@@ -54,7 +60,7 @@ import { render } from "ink";
54
60
  // src/tui/App.tsx
55
61
  import { spawn, spawnSync as spawnSync2 } from "child_process";
56
62
  import { homedir } from "os";
57
- import { basename as basename3, dirname, join as join4, sep } from "path";
63
+ import { basename as basename3, dirname, join as join3, sep } from "path";
58
64
  import { useEffect, useRef, useState as useState2 } from "react";
59
65
  import { Box as Box3, Text as Text3, useApp, useInput, useWindowSize } from "ink";
60
66
  import { lstatSync } from "fs";
@@ -102,6 +108,8 @@ function stateBadge(state) {
102
108
  return { icon: "\u2713", label: "in your skills too", color: color.green };
103
109
  case "repo skill, differs from library":
104
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 };
105
113
  case "available":
106
114
  return { icon: " ", label: "", color: color.faint };
107
115
  }
@@ -260,10 +268,11 @@ function loadSnapshot() {
260
268
  library,
261
269
  machine,
262
270
  issues: findIssues(projects, machine, new Set(library.map((l) => l.name)), statusOf),
263
- backups: listBackups(),
271
+ backups: [...listBackups(), ...pluginBackups()].sort((a, b) => b.movedAt.localeCompare(a.movedAt)),
264
272
  roots: readConfig().roots,
265
273
  harnesses: enabledHarnesses(),
266
- harnessesChosen: harnessesChosen()
274
+ harnessesChosen: harnessesChosen(),
275
+ keptGlobal: keptGlobal()
267
276
  };
268
277
  }
269
278
  function projectRows(root, library, status = projectStatus(root)) {
@@ -281,6 +290,7 @@ function projectRows(root, library, status = projectStatus(root)) {
281
290
  location: s.location,
282
291
  path: s.path,
283
292
  visibility: s.visibility,
293
+ ...s.source ? { source: s.source } : {},
284
294
  description: library.find((l) => l.name === s.name)?.description ?? readSkillInfo(s.path).description
285
295
  })
286
296
  );
@@ -351,6 +361,7 @@ var NEXT_MODE = { move: "keep", keep: "delete", delete: "move" };
351
361
  var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
352
362
  function CleanupWizard({
353
363
  skills,
364
+ kept,
354
365
  projects,
355
366
  usesIn,
356
367
  usesTotal,
@@ -367,11 +378,19 @@ function CleanupWizard({
367
378
  const [step, setStep] = useState("choose");
368
379
  const [focus, setFocus] = useState("skills");
369
380
  const [modes, setModes] = useState(
370
- () => new Map(skills.map((s) => [s.path, !initialSkill || s.name === initialSkill ? "move" : "keep"]))
381
+ () => new Map(skills.map((s) => [s.path, s.name === initialSkill || !initialSkill && !kept.has(s.name) ? "move" : "keep"]))
371
382
  );
372
383
  const modeOf = (s) => modes.get(s.path) ?? "keep";
373
- const setMode = (s, mode) => setModes((prev) => new Map(prev).set(s.path, mode));
374
- const [skillIndex, setSkillIndex] = useState(() => Math.max(0, skills.findIndex((s) => s.name === initialSkill)));
384
+ const [touched, setTouched] = useState(() => new Set(initialSkill ? [] : skills.map((s) => s.path)));
385
+ const setMode = (s, mode) => {
386
+ setModes((prev) => new Map(prev).set(s.path, mode));
387
+ setTouched((prev) => new Set(prev).add(s.path));
388
+ };
389
+ const [showKept, setShowKept] = useState(false);
390
+ const shown = (on) => skills.filter((s) => on || !kept.has(s.name) || s.name === initialSkill);
391
+ const list = shown(showKept);
392
+ const hiddenKept = skills.length - shown(false).length;
393
+ const [skillIndex, setSkillIndex] = useState(() => Math.max(0, list.findIndex((s) => s.name === initialSkill)));
375
394
  const [projectIndex, setProjectIndex] = useState(0);
376
395
  const [status, setStatus] = useState("");
377
396
  const [targets, setTargets] = useState(
@@ -379,9 +398,10 @@ function CleanupWizard({
379
398
  skills.map((s) => [s.path, new Set(projects.filter((p) => installedIn(s.name).includes(p) || (usesIn(s.name, p) ?? 0) > 0))])
380
399
  )
381
400
  );
382
- const skill = skills[skillIndex];
401
+ const skill = list[skillIndex];
383
402
  const chosen = skills.filter((s) => modeOf(s) === "move");
384
403
  const toDelete = skills.filter((s) => modeOf(s) === "delete");
404
+ const toKeep = skills.filter((s) => modeOf(s) === "keep" && touched.has(s.path) && !kept.has(s.name));
385
405
  const totalInstalls = chosen.reduce((n, s) => n + (targets.get(s.path)?.size ?? 0), 0);
386
406
  const orphans = chosen.filter((s) => (targets.get(s.path)?.size ?? 0) === 0);
387
407
  const toggleTarget = (s, project) => setTargets((prev) => {
@@ -417,7 +437,9 @@ function CleanupWizard({
417
437
  if (r.ok) unloaded++;
418
438
  else problems.push(`${s.name}: ${r.reason}`);
419
439
  }
420
- onDone({ unloaded, installs, deleted, problems });
440
+ setKeepGlobal(toKeep.map((s) => s.name), true);
441
+ setKeepGlobal([...chosen, ...toDelete].map((s) => s.name).filter((n) => kept.has(n)), false);
442
+ onDone({ unloaded, installs, deleted, kept: toKeep.length, problems });
421
443
  };
422
444
  inputRef.current = (input, key) => {
423
445
  if (key.ctrl && input === "c") return onCancel();
@@ -427,14 +449,19 @@ function CleanupWizard({
427
449
  return;
428
450
  }
429
451
  if (key.escape) return onCancel();
430
- if (input === "p") return setStatus(onReviewPrompt(skills));
431
- if (key.return) return chosen.length || toDelete.length ? setStep("review") : void 0;
452
+ if (input === "p") return setStatus(onReviewPrompt(list));
453
+ if (input === "k" && hiddenKept) {
454
+ const next = shown(!showKept);
455
+ setShowKept(!showKept);
456
+ return setSkillIndex(Math.max(0, next.findIndex((s) => s.path === skill?.path)));
457
+ }
458
+ if (key.return) return chosen.length || toDelete.length || toKeep.length ? setStep("review") : void 0;
432
459
  if (key.tab || key.rightArrow || key.leftArrow) {
433
460
  return setFocus(key.leftArrow ? "skills" : key.rightArrow ? "projects" : focus === "skills" ? "projects" : "skills");
434
461
  }
435
462
  if (focus === "skills") {
436
463
  if (key.upArrow) return setSkillIndex((i) => Math.max(0, i - 1));
437
- if (key.downArrow) return setSkillIndex((i) => Math.min(skills.length - 1, i + 1));
464
+ if (key.downArrow) return setSkillIndex((i) => Math.min(list.length - 1, i + 1));
438
465
  if (input === " " && skill) return setMode(skill, NEXT_MODE[modeOf(skill)]);
439
466
  if (input === "d" && skill) return setMode(skill, modeOf(skill) === "delete" ? "keep" : "delete");
440
467
  } else {
@@ -460,8 +487,9 @@ function CleanupWizard({
460
487
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", borderStyle: "round", borderColor: color.accent, paddingX: 2, paddingY: 1, height: bodyHeight + 2, children: [
461
488
  /* @__PURE__ */ jsx2(Text2, { color: color.text, children: [
462
489
  chosen.length ? `${plural(chosen.length, "skill")} ${chosen.length === 1 ? "moves" : "move"} into your projects (${plural(totalInstalls, "install")})` : "",
463
- toDelete.length ? `${plural(toDelete.length, "skill")} ${toDelete.length === 1 ? "is" : "are"} deleted` : ""
464
- ].filter(Boolean).join(" \xB7 ") + ". None of them will load in every repo anymore." }),
490
+ toDelete.length ? `${plural(toDelete.length, "skill")} ${toDelete.length === 1 ? "is" : "are"} deleted` : "",
491
+ toKeep.length ? `${plural(toKeep.length, "skill")} ${toKeep.length === 1 ? "stays" : "stay"} global on purpose` : ""
492
+ ].filter(Boolean).join(" \xB7 ") + "." }),
465
493
  /* @__PURE__ */ jsx2(Text2, { color: color.faint, children: "Moved skills are copied into Your skills first. Every original goes to a backup you can restore from Health." }),
466
494
  /* @__PURE__ */ jsx2(Text2, { children: " " }),
467
495
  lines.slice(0, Math.max(1, bodyHeight - 7)).map(({ s, where }) => /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate-end", children: [
@@ -471,6 +499,7 @@ function CleanupWizard({
471
499
  ] }, s.path)),
472
500
  lines.length > bodyHeight - 7 ? /* @__PURE__ */ jsx2(Text2, { color: color.faint, children: `\u2026 and ${lines.length - (bodyHeight - 7)} more` }) : null,
473
501
  toDelete.length ? /* @__PURE__ */ jsx2(Text2, { color: color.red, wrap: "truncate-end", children: `\u2715 delete (not kept in Your skills): ${toDelete.map((s) => s.name).join(", ")}` }) : null,
502
+ toKeep.length ? /* @__PURE__ */ jsx2(Text2, { color: color.muted, wrap: "truncate-end", children: `\u2713 keep global, and skip next cleanup: ${toKeep.map((s) => s.name).join(", ")}` }) : null,
474
503
  /* @__PURE__ */ jsx2(Box2, { flexGrow: 1 }),
475
504
  orphans.length ? /* @__PURE__ */ jsx2(Text2, { color: color.yellow, children: `${plural(orphans.length, "skill")} won't be in any project \u2014 add them later from "Add skills".` }) : null
476
505
  ] }),
@@ -490,7 +519,7 @@ function CleanupWizard({
490
519
  const detailHeight = height >= 26 ? 7 : height >= 20 ? 5 : 0;
491
520
  const listHeight = bodyHeight - detailHeight;
492
521
  const usesText = (n) => n === null ? "\u2026" : n === 0 ? "unused" : `${n} use${n === 1 ? "" : "s"}`;
493
- const skillRows = skills.map((s) => {
522
+ const skillRows = list.map((s) => {
494
523
  const mode = modeOf(s);
495
524
  const n = targets.get(s.path)?.size ?? 0;
496
525
  return {
@@ -500,7 +529,7 @@ function CleanupWizard({
500
529
  { text: s.name, grow: true, color: mode === "keep" ? color.muted : mode === "delete" ? color.red : void 0 },
501
530
  { text: usesText(usesTotal(s.name)).padStart(9), width: 9, color: usesTotal(s.name) ? color.text : color.faint },
502
531
  {
503
- text: mode === "keep" ? " keep global" : mode === "delete" ? " delete" : n ? ` \u2192 ${plural(n, "project")}` : " \u2192 no project",
532
+ text: mode === "keep" ? kept.has(s.name) ? " \u2713 kept global" : " keep global" : mode === "delete" ? " delete" : n ? ` \u2192 ${plural(n, "project")}` : " \u2192 no project",
504
533
  width: 15,
505
534
  color: mode === "keep" ? color.faint : mode === "delete" ? color.red : n ? color.green : color.yellow
506
535
  }
@@ -526,19 +555,19 @@ function CleanupWizard({
526
555
  ] }),
527
556
  /* @__PURE__ */ jsxs2(Box2, { height: 2, paddingX: 1, flexDirection: "column", children: [
528
557
  /* @__PURE__ */ jsx2(Text2, { color: color.text, wrap: "truncate-end", children: "These load in every repo, so every agent reads them all the time. Pick the projects that should keep each one." }),
529
- /* @__PURE__ */ jsx2(Text2, { color: color.faint, wrap: "truncate-end", children: status ? `\u2713 ${status}` : "\u25C9 move into the ticked projects \xB7 \u25CB keep global \xB7 \u2715 delete. Pre-ticked: projects that have it or used it. p: ask an agent." })
558
+ /* @__PURE__ */ jsx2(Text2, { color: color.faint, wrap: "truncate-end", children: status ? `\u2713 ${status}` : "\u25C9 move into the ticked projects \xB7 \u25CB keep global (remembered) \xB7 \u2715 delete. Pre-ticked: projects that have it or used it. p: ask an agent." })
530
559
  ] }),
531
560
  /* @__PURE__ */ jsxs2(Box2, { height: listHeight, children: [
532
561
  /* @__PURE__ */ jsx2(
533
562
  ListPanel,
534
563
  {
535
- title: `Global skills \xB7 ${chosen.length} move \xB7 ${toDelete.length} delete \xB7 uses ${usageDays}d`,
564
+ title: `Global skills \xB7 ${chosen.length} move \xB7 ${toDelete.length} delete${hiddenKept && !showKept ? ` \xB7 ${hiddenKept} kept hidden` : ""} \xB7 uses ${usageDays}d`,
536
565
  focused: focus === "skills",
537
566
  width: leftWidth,
538
567
  height: listHeight,
539
568
  rows: skillRows,
540
569
  selected: skillIndex,
541
- empty: "None of your skills load globally. \u{1F389}"
570
+ empty: hiddenKept ? `All reviewed: you keep ${plural(hiddenKept, "skill")} global on purpose. k shows them.` : "None of your skills load globally. \u{1F389}"
542
571
  }
543
572
  ),
544
573
  /* @__PURE__ */ jsx2(
@@ -571,6 +600,7 @@ function CleanupWizard({
571
600
  ["space", focus === "skills" ? "move / keep / delete" : "tick project"],
572
601
  ...focus === "skills" ? [["d", "delete"]] : [],
573
602
  ["tab", "switch side"],
603
+ ...hiddenKept ? [["k", showKept ? "hide kept" : `show kept (${hiddenKept})`]] : [],
574
604
  ["enter", `review (${plural(totalInstalls, "install")})`],
575
605
  ["p", "ask an agent"],
576
606
  ["esc", "cancel"]
@@ -602,17 +632,18 @@ var HELP_TOPICS = [
602
632
  "tab switch lists inside a project",
603
633
  "esc clear the search, then go back to the left pane",
604
634
  "ctrl+r reload ctrl+c quit",
605
- "p in the cleanup wizard: copy a prompt asking an agent to review the skills"
635
+ "p in the cleanup wizard: copy a prompt asking an agent to review the skills",
636
+ "k in the cleanup wizard: show / hide the skills you keep global on purpose"
606
637
  ]
607
638
  },
608
639
  {
609
640
  heading: "Badges",
610
641
  lines: [
611
642
  "\u273B \u2B21 \u25CE \u2124 Claude Code, Cursor, Codex, Zed \u2014 lit when that agent loads the skill here",
612
- "\xB2 Cursor reaches it through two folders (it reads several)",
613
- "\u26A0 loaded globally loads in every repo on this machine",
614
- "\u29C9 N copies the same skill reaches agents from N places \u2014 keep one",
615
- "\u25CF in sync \xB7 \u2191 update \xB7 \u270E edited \xB7 \u25CB only here",
643
+ "\u26A0 loaded globally loads in every repo on this machine, and you haven't decided about it yet",
644
+ "\u2713 global loads in every repo, and you keep it global on purpose (Enter \u2192 Keep global on purpose)",
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",
616
647
  "\u2713 committed in git, so teammates get it \xB1 changed committed, with local edits",
617
648
  "+ not added not committed yet \u2205 ignored gitignored \u2014 only on your machine"
618
649
  ]
@@ -629,8 +660,9 @@ var HELP_TOPICS = [
629
660
  {
630
661
  heading: "In a repo (only agents working in that repo see them)",
631
662
  lines: [
632
- ".claude/skills Claude Code (Cursor reads it too). Where skilllib installs your skills.",
633
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.",
634
666
  ".cursor/skills Cursor only. .codex/skills Cursor only (compatibility)."
635
667
  ]
636
668
  },
@@ -710,7 +742,7 @@ var HELP_TOPICS = [
710
742
  "Compat .claude/skills, .codex/skills, ~/.claude/skills, ~/.codex/skills",
711
743
  "Plus plugins from its marketplace, and Cursor's built-in skills",
712
744
  "Cloud Agents only see ~/.cursor/skills.",
713
- "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."
714
746
  ]
715
747
  },
716
748
  {
@@ -736,7 +768,7 @@ var HELP_TOPICS = [
736
768
  "User ~/.agents/skills",
737
769
  "Admin /etc/codex/skills",
738
770
  "System skills bundled by OpenAI",
739
- "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."
740
772
  ]
741
773
  },
742
774
  {
@@ -777,57 +809,18 @@ var HELP_TOPICS = [
777
809
  }
778
810
  ];
779
811
 
780
- // src/git.ts
781
- import { execFileSync } from "child_process";
782
- import { existsSync as existsSync2 } from "fs";
783
- import { join as join2, relative } from "path";
784
- function git(root, args, input) {
785
- try {
786
- return execFileSync("git", args, { cwd: root, encoding: "utf-8", input, stdio: ["pipe", "pipe", "ignore"], maxBuffer: 16 * 1024 * 1024 });
787
- } catch (err) {
788
- const out = err.stdout;
789
- return typeof out === "string" ? out : null;
790
- }
791
- }
792
- function gitInfo(root) {
793
- if (!existsSync2(join2(root, ".git"))) return null;
794
- const dirs = [...ALL_PROJECT_DIRS, MANIFEST_FILE].filter((d) => existsSync2(join2(root, d)));
795
- if (dirs.length === 0) return { of: () => "new" };
796
- const tracked = (git(root, ["ls-files", "-z", "--", ...dirs]) ?? "").split("\0").filter(Boolean);
797
- const dirty = (git(root, ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...dirs]) ?? "").split("\0").filter(Boolean).map((entry) => entry.slice(3));
798
- const ignoredCache = /* @__PURE__ */ new Map();
799
- const isIgnored = (rel) => {
800
- if (!ignoredCache.has(rel)) {
801
- const out = git(root, ["check-ignore", "--no-index", "--", rel]) ?? "";
802
- ignoredCache.set(rel, out.trim().length > 0);
803
- }
804
- return ignoredCache.get(rel);
805
- };
806
- const under = (list, rel) => list.some((p) => p === rel || p.startsWith(rel + "/"));
807
- return {
808
- of: (relPath) => {
809
- const rel = relPath.replace(/\/+$/, "");
810
- if (under(tracked, rel)) return under(dirty, rel) ? "changed" : "committed";
811
- return isIgnored(rel) ? "ignored" : "new";
812
- }
813
- };
814
- }
815
- function relativeTo(root, path) {
816
- return relative(root, path).split("\\").join("/");
817
- }
818
-
819
812
  // src/review.ts
820
813
  import { spawnSync } from "child_process";
821
- import { existsSync as existsSync3, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
822
- 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";
823
816
  var EXCERPT_CHARS = 1200;
824
817
  function tildify(p) {
825
818
  const home = userHome();
826
819
  return p.startsWith(home) ? "~" + p.slice(home.length) : p;
827
820
  }
828
821
  function skillExcerpt(dir, chars) {
829
- const file = join3(dir, "SKILL.md");
830
- 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 };
831
824
  const content = readFileSync2(file, "utf-8").replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "");
832
825
  let files = 0;
833
826
  try {
@@ -851,6 +844,7 @@ function buildReviewPrompt(skills, ctx) {
851
844
  `- Location: ${tildify(s.path)}${s.links.length ? ` (also linked from ${s.links.map(tildify).join(", ")})` : ""}`,
852
845
  `- Loaded by: ${s.loadedBy}`,
853
846
  ...s.vendor ? [`- Vendor-managed: ${s.vendor}`] : [],
847
+ ...s.keptGlobal ? ["- I marked this as global on purpose: recommend changing that only for a strong reason (unused, duplicated, low quality)"] : [],
854
848
  `- Usage (Claude Code transcripts only): ${usage}`,
855
849
  `- Installed in projects: ${s.installedIn.length ? s.installedIn.join(", ") : "none"}`,
856
850
  `- Other copies agents can see: ${s.otherCopies.length ? s.otherCopies.join("; ") : "none"}`,
@@ -897,7 +891,7 @@ Reply with a short table (skill \xB7 decision \xB7 projects \xB7 one-line reason
897
891
  `;
898
892
  }
899
893
  function copyText(text) {
900
- const savedTo = join3(skilllibHome(), "review-prompt.md");
894
+ const savedTo = join2(skilllibHome(), "review-prompt.md");
901
895
  mkdirSync(skilllibHome(), { recursive: true });
902
896
  writeFileSync(savedTo, text);
903
897
  const tools = process.platform === "darwin" ? [["pbcopy", []]] : process.platform === "win32" ? [["clip", []]] : [
@@ -1037,15 +1031,18 @@ function versionText(r) {
1037
1031
  }
1038
1032
  var harnessName = (id) => HARNESSES.find((h) => h.id === id)?.name ?? id;
1039
1033
  function harnessChips(visibility, installed) {
1040
- return visibility.flatMap((v) => {
1034
+ return visibility.map((v) => {
1041
1035
  const h = HARNESSES.find((x) => x.id === v.id);
1042
1036
  const on = installed && v.paths > 0;
1043
- return [
1044
- { text: ` ${h?.icon ?? "?"}`, color: on ? h?.color : color.faint, dim: !on },
1045
- { text: v.paths > 1 && installed ? "\xB2" : " ", color: color.yellow }
1046
- ];
1037
+ return { text: ` ${h?.icon ?? "?"} `, color: on ? h?.color : color.faint, dim: !on };
1047
1038
  });
1048
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
+ };
1049
1046
  var GIT_LABEL = {
1050
1047
  committed: { text: "\u2713 committed", color: color.green },
1051
1048
  changed: { text: "\xB1 changed", color: color.yellow },
@@ -1091,7 +1088,7 @@ function wrapAligned(line, width) {
1091
1088
  function hasLinks(root, name) {
1092
1089
  return ALL_PROJECT_DIRS.some((d) => {
1093
1090
  try {
1094
- return lstatSync(join4(root, d, name)).isSymbolicLink();
1091
+ return lstatSync(join3(root, d, name)).isSymbolicLink();
1095
1092
  } catch {
1096
1093
  return false;
1097
1094
  }
@@ -1256,8 +1253,8 @@ function App() {
1256
1253
  const pKey = placeKey(place);
1257
1254
  function projectSections(root) {
1258
1255
  const where = basename3(root);
1259
- const git2 = gitFor(root);
1260
- 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;
1261
1258
  const rows = snapshot.rows.get(root) ?? [];
1262
1259
  const projectActions = [
1263
1260
  {
@@ -1310,7 +1307,8 @@ function App() {
1310
1307
  const dir = r.path;
1311
1308
  const badge = stateBadge(r.state);
1312
1309
  const edited = r.state.startsWith("edited");
1313
- 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;
1314
1312
  const missing = r.visibility.filter((v) => v.paths === 0);
1315
1313
  const seenBy = r.visibility.filter((v) => v.paths > 0);
1316
1314
  const notInLibrary = r.state === "local only" || r.state === "untracked, differs from library" || r.state === "repo skill" || r.state === "repo skill, differs from library";
@@ -1351,7 +1349,10 @@ function App() {
1351
1349
  ...r.installed && missing.length ? [{ label: `Make visible to ${missing.map((v) => harnessName(v.id)).join(", ")}`, hint: "adds links; one real copy", run: () => makeVisible() }] : [],
1352
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 }] : []
1353
1351
  ];
1354
- 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) {
1355
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}`) };
1356
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 };
1357
1358
  actions.push(libraryAction, ...visibilityActions);
@@ -1411,7 +1412,7 @@ function App() {
1411
1412
  }
1412
1413
  if (!inRepo) actions.push(...visibilityActions);
1413
1414
  actions.push(
1414
- { label: "Edit SKILL.md", run: () => void edit(join4(dir, "SKILL.md")) },
1415
+ { label: "Edit SKILL.md", run: () => void edit(join3(dir, "SKILL.md")) },
1415
1416
  { label: "Open folder", run: () => openFolder(dir) },
1416
1417
  ...projectActions
1417
1418
  );
@@ -1420,11 +1421,11 @@ function App() {
1420
1421
  return {
1421
1422
  key,
1422
1423
  search: r.name,
1423
- 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),
1424
1425
  skill: { name: r.name, path: dir },
1425
1426
  tags: [
1426
1427
  r.installed ? "in this project" : "installable",
1427
- ...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"] : []
1428
1429
  ],
1429
1430
  row: {
1430
1431
  key,
@@ -1435,11 +1436,11 @@ function App() {
1435
1436
  },
1436
1437
  { text: r.name, grow: true, color: r.installed ? void 0 : color.muted },
1437
1438
  { text: whereLabel.padStart(8), width: 8, color: color.faint },
1438
- ...gitCell(r.installed ? gitOf(r.path) : null, !!git2),
1439
+ ...gitCell(r.installed ? gitOf(r.path) : null, !!git),
1439
1440
  ...harnessChips(r.visibility.length ? r.visibility : enabledIds.map((id) => ({ id, paths: 0 })), r.installed),
1440
1441
  { text: versionText(r).padStart(8), width: 8, color: behind ? color.yellow : color.muted },
1441
1442
  {
1442
- 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}`,
1443
1444
  width: 19,
1444
1445
  color: r.installed && dupes(r.name) > 1 ? color.red : !r.installed && globalCopy ? color.yellow : badge.color
1445
1446
  },
@@ -1453,12 +1454,12 @@ function App() {
1453
1454
  dir,
1454
1455
  meta: [
1455
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",
1456
- 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",
1457
- ...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)]] : [],
1458
1459
  r.inLibrary ? `in Your skills (v${r.latest}${origin ? `, from ${origin}` : ""})` : "not in Your skills",
1459
1460
  `${usesText(r.name, root)} Claude Code uses here (${USAGE_DAYS}d)`
1460
1461
  ],
1461
- ...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).` } : {}
1462
1463
  }
1463
1464
  };
1464
1465
  };
@@ -1470,6 +1471,7 @@ function App() {
1470
1471
  const dupes = (name) => copiesOf(name).length;
1471
1472
  const globalItem = (m) => {
1472
1473
  const inLib = libraryNames.has(m.name);
1474
+ const kept = m.movable && snapshot.keptGlobal.has(m.name);
1473
1475
  const alsoLocal = rows.some((r) => r.installed && r.name === m.name);
1474
1476
  const moveHere = () => confirm(
1475
1477
  `Use ${m.name} only where you add it? It's ${inLib ? "" : "imported into the library, "}added to ${where}, and stops loading globally \u2014 other projects lose it until you add it there. Restorable from Health.`,
@@ -1483,6 +1485,7 @@ function App() {
1483
1485
  );
1484
1486
  const actions = m.movable ? [
1485
1487
  { label: "Choose which repos keep it\u2026", hint: "cleanup wizard", run: () => setWizard({ initialSkill: m.name }) },
1488
+ keepAction(m),
1486
1489
  { label: `Use only in ${where}`, hint: "add here, stop loading everywhere", run: moveHere },
1487
1490
  { label: "Delete", hint: "not kept in Your skills \xB7 restorable from Health", run: () => confirmDeleteGlobal([m]) },
1488
1491
  ...inLib ? [] : [
@@ -1507,13 +1510,13 @@ function App() {
1507
1510
  { text: "\u25C8 ", color: color.accentDim },
1508
1511
  { text: m.name, grow: true },
1509
1512
  { text: (m.kind === "global" || m.kind === "skills.sh" ? "global" : m.kind).padStart(8), width: 8, color: color.faint },
1510
- ...gitCell(null, !!git2),
1513
+ ...gitCell(null, !!git),
1511
1514
  ...harnessChips(enabledIds.map((id) => ({ id, paths: m.harnesses.includes(id) ? 1 : 0 })), true),
1512
1515
  { text: "", width: 8 },
1513
1516
  {
1514
- text: ` ${dupes(m.name) > 1 ? `\u29C9 ${dupes(m.name)} copies` : m.movable ? "\u26A0 loaded globally" : "vendor"}`,
1517
+ text: ` ${dupes(m.name) > 1 ? `\u29C9 ${dupes(m.name)} copies` : kept ? "\u2713 global" : m.movable ? "\u26A0 loaded globally" : "vendor"}`,
1515
1518
  width: 19,
1516
- color: dupes(m.name) > 1 ? color.red : m.movable ? color.yellow : color.faint
1519
+ color: dupes(m.name) > 1 ? color.red : kept ? color.green : m.movable ? color.yellow : color.faint
1517
1520
  },
1518
1521
  { text: usesText(m.name, root).padStart(4), color: color.muted }
1519
1522
  ]
@@ -1526,9 +1529,9 @@ function App() {
1526
1529
  meta: [
1527
1530
  `loaded in every project by ${m.harnesses.map(harnessName).join(", ")}`,
1528
1531
  `${m.kind}: ${m.origin}`,
1529
- m.movable ? "yours: the cleanup wizard can scope it to projects" : "managed by the vendor"
1532
+ kept ? "yours: kept global on purpose" : m.movable ? "yours: the cleanup wizard can scope it to projects" : "managed by the vendor"
1530
1533
  ],
1531
- 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
1532
1535
  }
1533
1536
  };
1534
1537
  };
@@ -1573,15 +1576,28 @@ function App() {
1573
1576
  preview: {
1574
1577
  title: "Make every skill usable by all your agents",
1575
1578
  meta: [`${plural2(partial.length, "skill")} missing for ${missingAgents.map(harnessName).join(", ")}`],
1576
- 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.`
1577
1580
  }
1578
1581
  }
1579
1582
  ] : [];
1580
1583
  const fromLibrary = local.filter((r) => r.managed || r.state === "untracked copy of library skill");
1581
- 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
+ ];
1582
1596
  const yours = globals.filter((m) => m.movable);
1597
+ const keptYours = yours.filter((m) => snapshot.keptGlobal.has(m.name));
1598
+ const unreviewed = yours.filter((m) => !snapshot.keptGlobal.has(m.name));
1583
1599
  const vendor = globals.filter((m) => !m.movable);
1584
- const cleanupItem = yours.length ? [
1600
+ const cleanupItem = unreviewed.length ? [
1585
1601
  {
1586
1602
  key: "__cleanup",
1587
1603
  search: "clean up global",
@@ -1590,7 +1606,7 @@ function App() {
1590
1606
  key: "__cleanup",
1591
1607
  cells: [
1592
1608
  { text: "\u26A0 ", color: color.yellow },
1593
- { text: `${plural2(yours.length, "skill")} of yours load in every repo \u2014 choose where each should live\u2026`, grow: true, color: color.yellow }
1609
+ { text: `${plural2(unreviewed.length, "skill")} of yours load in every repo \u2014 choose where each should live\u2026`, grow: true, color: color.yellow }
1594
1610
  ]
1595
1611
  },
1596
1612
  primary: { label: "clean up", run: () => setWizard({}) },
@@ -1619,43 +1635,17 @@ function App() {
1619
1635
  }
1620
1636
  ]
1621
1637
  },
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 },
1622
1640
  {
1623
- title: `The repo's own (${repoOwn.length})`,
1624
- items: repoOwn.map(item),
1625
- groupActions: (members) => [
1626
- {
1627
- label: `Copy all ${members.length} into your library`,
1628
- hint: "the repo keeps its copies",
1629
- run: () => act(() => {
1630
- const added = members.map((m) => importSkill(m.skill.path, { force: true })).filter((r) => r.status !== "unchanged").length;
1631
- return ok(`${plural2(added, "skill")} copied into your library`);
1632
- })
1633
- }
1634
- ]
1641
+ title: `\u26A0 Global \xB7 yours, not reviewed, loaded in every repo (${unreviewed.length})`,
1642
+ items: [...cleanupItem, ...unreviewed.map(globalItem)],
1643
+ groupActions: (members) => [...keepGroupActions(members), ...yourGlobalActions(members)]
1635
1644
  },
1636
1645
  {
1637
- title: `\u26A0 Global \xB7 yours, loaded in every repo (${yours.length})`,
1638
- items: [...cleanupItem, ...yours.map(globalItem)],
1639
- groupActions: (members) => [
1640
- {
1641
- label: `Copy review prompt for all ${members.length}`,
1642
- hint: "for an agent to decide keep / move / delete",
1643
- run: () => copyReviewPrompt(snapshot.machine.filter((m) => members.some((x) => x.skill?.path === m.path)).map(reviewOfGlobal), `These are global skills (group ${members[0]?.group?.label ?? ""}).`)
1644
- },
1645
- {
1646
- label: `Copy all ${members.length} to Your skills`,
1647
- hint: "they keep loading globally",
1648
- run: () => act(() => {
1649
- const added = members.map((m) => importSkill(m.skill.path)).filter((r) => r.status === "added").length;
1650
- return ok(`${plural2(added, "skill")} imported into your library`);
1651
- })
1652
- },
1653
- {
1654
- label: `Delete all ${members.length}`,
1655
- hint: "not kept in Your skills \xB7 restorable from Health",
1656
- run: () => confirmDeleteGlobal(snapshot.machine.filter((m) => members.some((x) => x.skill?.path === m.path)))
1657
- }
1658
- ]
1646
+ title: `\u2713 Global \xB7 yours, on purpose (${keptYours.length})`,
1647
+ items: keptYours.map(globalItem),
1648
+ groupActions: (members) => [...keepGroupActions(members), ...yourGlobalActions(members)]
1659
1649
  },
1660
1650
  {
1661
1651
  title: `Global \xB7 from vendors: plugins, claude.ai, built-in (${vendor.length})`,
@@ -1738,7 +1728,7 @@ function App() {
1738
1728
  primary: { label: "choose projects", run: toggleProjects.run },
1739
1729
  actions: [
1740
1730
  toggleProjects,
1741
- { 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")) },
1742
1732
  { label: "Open folder", run: () => openFolder(librarySkillDir(l.name)) },
1743
1733
  {
1744
1734
  label: "Copy review prompt",
@@ -1795,6 +1785,8 @@ function App() {
1795
1785
  }
1796
1786
  function globalSections() {
1797
1787
  const yours = snapshot.machine.filter((m) => m.movable);
1788
+ const unreviewed = yours.filter((m) => !m.broken && !snapshot.keptGlobal.has(m.name));
1789
+ const keptCount = yours.filter((m) => !m.broken && snapshot.keptGlobal.has(m.name)).length;
1798
1790
  const importAll = {
1799
1791
  label: "Copy all of yours to Your skills",
1800
1792
  run: () => {
@@ -1833,12 +1825,16 @@ function App() {
1833
1825
  key: "__cleanup",
1834
1826
  cells: [
1835
1827
  { text: "\u26A0 ", color: color.yellow },
1836
- { text: `Clean up: choose which repos keep each of your ${yours.length} global skills\u2026`, grow: true, color: color.yellow }
1828
+ { text: `Clean up: choose which repos keep each of your ${unreviewed.length} unreviewed global skills\u2026`, grow: true, color: color.yellow }
1837
1829
  ]
1838
1830
  },
1839
1831
  primary: { label: "clean up", run: () => setWizard({}) },
1840
1832
  actions: [{ label: "Open the cleanup wizard", run: () => setWizard({}) }],
1841
- preview: { title: "Clean up global skills", meta: ["every agent reads global skills in every repo"], description: "Install each skill only where it's needed, then stop loading it everywhere. Originals are kept in backup." }
1833
+ preview: {
1834
+ title: "Clean up global skills",
1835
+ meta: ["every agent reads global skills in every repo", ...keptCount ? [`skips the ${keptCount} you keep global on purpose (k shows them)`] : []],
1836
+ description: "Install each skill only where it's needed, keep it global on purpose, or delete it. Originals are kept in backup."
1837
+ }
1842
1838
  };
1843
1839
  const grouped = groups.map(([title, test]) => ({
1844
1840
  title,
@@ -1849,6 +1845,7 @@ function App() {
1849
1845
  run: () => copyReviewPrompt(snapshot.machine.filter((m) => members.some((x) => x.skill?.path === m.path)).map(reviewOfGlobal), `These are global skills (group ${members[0]?.group?.label ?? ""}).`)
1850
1846
  },
1851
1847
  ...title.startsWith("Yours") ? [
1848
+ ...keepGroupActions(members),
1852
1849
  {
1853
1850
  label: `Delete all ${members.length}`,
1854
1851
  hint: "not kept in Your skills \xB7 restorable from Health",
@@ -1879,7 +1876,7 @@ function App() {
1879
1876
  actions.push({ label: "Remove the broken link", run: unload });
1880
1877
  } else {
1881
1878
  if (!inLib) actions.push({ label: "Copy to Your skills", hint: "keeps loading globally", run: importIt });
1882
- if (m.movable) actions.push({ label: "Stop loading it globally", hint: "use it per project instead", run: unload });
1879
+ if (m.movable) actions.push(keepAction(m), { label: "Stop loading it globally", hint: "use it per project instead", run: unload });
1883
1880
  primary = m.movable ? { label: "choose repos\u2026", run: () => setWizard({ initialSkill: m.name }) } : !inLib ? { label: "copy to your skills", run: importIt } : void 0;
1884
1881
  if (m.movable) {
1885
1882
  actions.unshift({ label: "Choose which repos keep it\u2026", hint: "cleanup wizard", run: () => setWizard({ initialSkill: m.name }) });
@@ -1901,6 +1898,7 @@ function App() {
1901
1898
  { text: m.name.padEnd(30).slice(0, 30), color: m.broken ? color.red : void 0 },
1902
1899
  { text: m.broken ? "broken link" : m.origin, grow: true, color: m.broken ? color.red : kindColor[m.kind] },
1903
1900
  ...harnessChips(enabledIds.map((id) => ({ id, paths: m.harnesses.includes(id) ? 1 : 0 })), !m.broken),
1901
+ { text: m.movable && snapshot.keptGlobal.has(m.name) ? " \u2713 kept" : " ", color: color.green },
1904
1902
  { text: inLib ? " \u2713 library" : " ", color: color.green },
1905
1903
  { text: usesText(m.name).padStart(5), color: color.muted }
1906
1904
  ]
@@ -1913,7 +1911,7 @@ function App() {
1913
1911
  meta: [
1914
1912
  m.harnesses.length ? `loaded everywhere by ${m.harnesses.map(harnessName).join(", ")}` : "not loaded by your harnesses",
1915
1913
  `${m.kind}: ${m.origin}`,
1916
- m.movable ? "yours to manage" : "managed by the vendor",
1914
+ m.movable ? snapshot.keptGlobal.has(m.name) ? "yours \xB7 kept global on purpose" : "yours to manage" : "managed by the vendor",
1917
1915
  tildify2(m.path) + (m.links.length ? ` (+ ${m.links.length} link${m.links.length > 1 ? "s" : ""})` : "")
1918
1916
  ],
1919
1917
  description: m.broken ? "Broken link: it points at a folder that no longer exists, so it loads nothing." : void 0
@@ -1952,7 +1950,48 @@ function App() {
1952
1950
  description: "The prompt includes each skill's description, a SKILL.md excerpt, how it was installed (vendor, skills.sh, plugin\u2026), which agents load it, usage per project, duplicates and your project list. Paste it into Claude Code, Cursor or Codex; it replies with keep / move / delete per skill."
1953
1951
  }
1954
1952
  };
1955
- return [{ title: "", items: [...yours.some((m) => !m.broken) ? [launcher] : [], reviewAll] }, ...grouped];
1953
+ return [{ title: "", items: [...unreviewed.length ? [launcher] : [], reviewAll] }, ...grouped];
1954
+ }
1955
+ function markKept(names, keep) {
1956
+ act(() => {
1957
+ setKeepGlobal(names, keep);
1958
+ const label = names.length === 1 ? names[0] : plural2(names.length, "skill");
1959
+ return ok(keep ? `${label} kept global on purpose \u2014 cleanup skips ${names.length === 1 ? "it" : "them"}` : `${label} back in cleanup`);
1960
+ });
1961
+ }
1962
+ function keepAction(m) {
1963
+ return snapshot.keptGlobal.has(m.name) ? { label: "Unmark: not global on purpose", hint: "cleanup asks about it again", run: () => markKept([m.name], false) } : { label: "\u2713 Keep global on purpose", hint: "stops the \u26A0 and skips it in cleanup", run: () => markKept([m.name], true) };
1964
+ }
1965
+ function keepGroupActions(members) {
1966
+ const names = [...new Set(members.flatMap((m) => m.skill ? [m.skill.name] : []))];
1967
+ const unkept = names.filter((n) => !snapshot.keptGlobal.has(n));
1968
+ const kept = names.filter((n) => snapshot.keptGlobal.has(n));
1969
+ return [
1970
+ ...unkept.length ? [{ label: `\u2713 Keep all ${unkept.length} global on purpose`, hint: "cleanup skips them", run: () => markKept(unkept, true) }] : [],
1971
+ ...kept.length ? [{ label: `Unmark all ${kept.length}`, hint: "cleanup asks about them again", run: () => markKept(kept, false) }] : []
1972
+ ];
1973
+ }
1974
+ function yourGlobalActions(members) {
1975
+ return [
1976
+ {
1977
+ label: `Copy review prompt for all ${members.length}`,
1978
+ hint: "for an agent to decide keep / move / delete",
1979
+ run: () => copyReviewPrompt(snapshot.machine.filter((m) => members.some((x) => x.skill?.path === m.path)).map(reviewOfGlobal), `These are global skills (group ${members[0]?.group?.label ?? ""}).`)
1980
+ },
1981
+ {
1982
+ label: `Copy all ${members.length} to Your skills`,
1983
+ hint: "they keep loading globally",
1984
+ run: () => act(() => {
1985
+ const added = members.map((m) => importSkill(m.skill.path)).filter((r) => r.status === "added").length;
1986
+ return ok(`${plural2(added, "skill")} imported into your library`);
1987
+ })
1988
+ },
1989
+ {
1990
+ label: `Delete all ${members.length}`,
1991
+ hint: "not kept in Your skills \xB7 restorable from Health",
1992
+ run: () => confirmDeleteGlobal(snapshot.machine.filter((m) => members.some((x) => x.skill?.path === m.path)))
1993
+ }
1994
+ ];
1956
1995
  }
1957
1996
  function confirmDeleteGlobal(skills) {
1958
1997
  if (skills.length === 0) return;
@@ -2001,7 +2040,8 @@ function App() {
2001
2040
  usesByProject: u.byProject,
2002
2041
  installedIn: projectsWith(m.name),
2003
2042
  otherCopies: snapshot.machine.filter((x) => x.name === m.name && x.path !== m.path).map((x) => `${x.kind} ${x.origin} (${tildify2(x.path)})`),
2004
- inYourSkills: lib ? `yes, v${lib.version}` : null
2043
+ inYourSkills: lib ? `yes, v${lib.version}` : null,
2044
+ keptGlobal: m.movable && snapshot.keptGlobal.has(m.name)
2005
2045
  };
2006
2046
  }
2007
2047
  function reviewOfLibrary(l) {
@@ -2116,7 +2156,12 @@ function App() {
2116
2156
  title: "Issues",
2117
2157
  items: snapshot.issues.map((issue) => {
2118
2158
  const fix = issue.fix;
2119
- 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;
2120
2165
  return {
2121
2166
  key: issue.id,
2122
2167
  search: issue.title,
@@ -2130,10 +2175,10 @@ function App() {
2130
2175
  ]
2131
2176
  },
2132
2177
  primary: run ? { label: "fix", run } : void 0,
2133
- actions: run && fix ? [{ label: fix.label, run }] : [],
2178
+ actions: fix && run ? [{ label: fix.label, run }] : choices,
2134
2179
  preview: {
2135
2180
  title: issue.title,
2136
- 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"],
2137
2182
  description: issue.detail
2138
2183
  }
2139
2184
  };
@@ -2143,7 +2188,7 @@ function App() {
2143
2188
  title: "Moved out \xB7 restorable",
2144
2189
  items: snapshot.backups.map((b) => {
2145
2190
  const restore = () => act(() => {
2146
- const r = restoreBackup(b);
2191
+ const r = b.kind === "plugin" ? restorePlugin(b) : restoreBackup(b);
2147
2192
  return r.ok ? ok(`${b.name} restored to ${tildify2(r.to)}`) : warn(`Can't restore ${b.name}: ${r.reason}`);
2148
2193
  });
2149
2194
  return {
@@ -2154,7 +2199,7 @@ function App() {
2154
2199
  key: b.path,
2155
2200
  cells: [
2156
2201
  { text: b.name, grow: true },
2157
- { text: b.kind === "trash" ? " deleted from library" : " unloaded from global", color: color.muted },
2202
+ { text: ` ${BACKUP_LABEL[b.kind]}`, color: color.muted },
2158
2203
  { text: ` ${b.movedAt}`, color: color.faint }
2159
2204
  ]
2160
2205
  },
@@ -2163,7 +2208,7 @@ function App() {
2163
2208
  preview: {
2164
2209
  title: b.name,
2165
2210
  dir: b.path,
2166
- 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}`]
2167
2212
  }
2168
2213
  };
2169
2214
  })
@@ -2214,7 +2259,7 @@ function App() {
2214
2259
  if (!r.ok) return say(`Can't create ${value}: ${r.reason}`, false);
2215
2260
  setSelected((s) => ({ ...s, "right:library": value.trim() }));
2216
2261
  reload();
2217
- void edit(join4(r.dir, "SKILL.md"));
2262
+ void edit(join3(r.dir, "SKILL.md"));
2218
2263
  }
2219
2264
  });
2220
2265
  }
@@ -2406,7 +2451,7 @@ function App() {
2406
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."
2407
2452
  ],
2408
2453
  help: ["Help", "Where skills come from, and how Claude Code, Cursor, Codex and Zed find and use them. Press enter (or ? anywhere)."],
2409
- 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."],
2410
2455
  settings: ["Settings", `Harnesses: ${snapshot.harnesses.map(harnessName).join(", ") || "none"} \xB7 Folders: ${snapshot.roots.map(tildify2).join(", ") || "none yet"}`]
2411
2456
  };
2412
2457
  const [title, description] = about[place.kind] ?? ["", ""];
@@ -2496,6 +2541,7 @@ function App() {
2496
2541
  CleanupWizard,
2497
2542
  {
2498
2543
  skills: yours,
2544
+ kept: snapshot.keptGlobal,
2499
2545
  projects: snapshot.projects,
2500
2546
  usesIn: (skill, project) => usage ? usage.byProject.get(project)?.get(skill) ?? 0 : null,
2501
2547
  usesTotal: (skill) => usage ? usage.total.get(skill) ?? 0 : null,
@@ -2524,7 +2570,12 @@ function App() {
2524
2570
  reload();
2525
2571
  setToast({
2526
2572
  ok: r.problems.length === 0,
2527
- text: `${plural2(r.unloaded, "skill")} moved into projects (${plural2(r.installs, "install")})${r.deleted ? ` \xB7 ${r.deleted} deleted` : ""}${r.problems.length ? ` \xB7 ${r.problems.length} kept global: ${r.problems[0]}` : ""}`
2573
+ text: [
2574
+ r.unloaded || r.installs ? `${plural2(r.unloaded, "skill")} moved into projects (${plural2(r.installs, "install")})` : "",
2575
+ r.deleted ? `${r.deleted} deleted` : "",
2576
+ r.kept ? `${plural2(r.kept, "skill")} kept global on purpose` : "",
2577
+ r.problems.length ? `${r.problems.length} left global: ${r.problems[0]}` : ""
2578
+ ].filter(Boolean).join(" \xB7 ")
2528
2579
  });
2529
2580
  }
2530
2581
  }