skilllib 1.11.3 → 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.
@@ -1,10 +1,13 @@
1
1
  import {
2
2
  HARNESSES,
3
3
  PROJECT_SKILLS_DIR,
4
+ addRefusal,
4
5
  addRoot,
5
6
  addSkill,
6
7
  agentSkillState,
7
8
  allowTrackedLinks,
9
+ blindSaid,
10
+ blindWithoutGlobal,
8
11
  claudeBinary,
9
12
  createSkill,
10
13
  cursorPlugins,
@@ -12,6 +15,7 @@ import {
12
15
  deleteLibrarySkill,
13
16
  discoverProjects,
14
17
  enabledHarnesses,
18
+ entryExists,
15
19
  findIssues,
16
20
  foldersOf,
17
21
  forgetClaudeBinary,
@@ -20,9 +24,14 @@ import {
20
24
  harness,
21
25
  harnessesChosen,
22
26
  importSkill,
27
+ inTheWaySaid,
23
28
  installAgentSkill,
29
+ installDirs,
24
30
  installedPlugins,
31
+ isDir,
32
+ isShared,
25
33
  keptGlobal,
34
+ knownProjects,
26
35
  latestVersion,
27
36
  libraryOrigins,
28
37
  librarySkillDir,
@@ -32,6 +41,8 @@ import {
32
41
  localStamp,
33
42
  machineSkills,
34
43
  moveGlobalSkills,
44
+ movedTogether,
45
+ notWritable,
35
46
  pluginBackups,
36
47
  projectHere,
37
48
  projectOfFactory,
@@ -41,9 +52,11 @@ import {
41
52
  readManifest,
42
53
  readSkillInfo,
43
54
  readableRoots,
55
+ realPath,
44
56
  recordOrigin,
45
57
  relativeTo,
46
58
  relinkDependency,
59
+ rememberProjects,
47
60
  removeAgentSkill,
48
61
  removeRoot,
49
62
  removeSkill,
@@ -56,18 +69,22 @@ import {
56
69
  setHidden,
57
70
  setKeepGlobal,
58
71
  setPluginOn,
72
+ skillOf,
59
73
  skilllibHome,
60
74
  skillsLoadedIn,
75
+ strandedLinks,
76
+ survives,
61
77
  tildify,
78
+ trackBlocker,
62
79
  treeHash,
63
80
  uninstallPlugin,
64
81
  updatePlugin,
65
82
  updateProject,
66
83
  usageIssues,
67
84
  versionHistory,
68
- visibilityOf,
69
- visibleProjects
70
- } from "./chunk-3XDWEHI4.js";
85
+ visibleProjects,
86
+ withoutForceHint
87
+ } from "./chunk-TE2EDZ7L.js";
71
88
 
72
89
  // src/tui/index.tsx
73
90
  import { render } from "ink";
@@ -186,6 +203,9 @@ function wrap(text, width, max) {
186
203
  return lines;
187
204
  }
188
205
 
206
+ // src/tui/prompts.ts
207
+ import { dirname } from "path";
208
+
189
209
  // src/review.ts
190
210
  import { spawnSync } from "child_process";
191
211
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
@@ -194,7 +214,7 @@ var EXCERPT_CHARS = 1200;
194
214
  function skillExcerpt(dir, chars) {
195
215
  const file = join(dir, "SKILL.md");
196
216
  if (!existsSync(file)) return { text: "(SKILL.md not found)", files: 0, size: 0 };
197
- const content = readFileSync(file, "utf-8").replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "");
217
+ const content = readFileSync(file, "utf-8").replace(/^\uFEFF/, "").replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "");
198
218
  let files = 0;
199
219
  try {
200
220
  files = readdirSync(dir, { recursive: true, withFileTypes: true }).filter((d) => d.isFile()).length;
@@ -296,8 +316,9 @@ var isFailure = (r) => typeof r !== "string" && "failed" in r;
296
316
  var followUp = (r) => typeof r !== "string" && "then" in r ? r.then : void 0;
297
317
  var said = (r) => typeof r === "string" ? r : r.message;
298
318
  var joined = (results) => {
299
- const message = results.map(said).join(" \xB7 ");
300
- return results.some(isFailure) ? failed(message) : message;
319
+ const bad = results.filter(isFailure);
320
+ const message = [...bad.length && bad.length < results.length ? [`${results.length - bad.length} of ${results.length} done`] : [], ...results.map(said)].join(" \xB7 ");
321
+ return bad.length ? failed(message) : message;
301
322
  };
302
323
  var SEVERITY_RANK = { problem: 0, suggestion: 1 };
303
324
  function fixOf(c, found) {
@@ -325,6 +346,7 @@ function shortOf(found) {
325
346
  sync: "Folder missing",
326
347
  unavailable: "Pinned, not in your library",
327
348
  pin: "Pinned version not in your library",
349
+ "out-of-sync": "Not the pinned version",
328
350
  update: "Out of date",
329
351
  uncommitted: "Commit it first",
330
352
  local: "Not in your library",
@@ -353,7 +375,7 @@ function viewOf(found) {
353
375
  title: found.title,
354
376
  short: shortOf(found),
355
377
  detail: found.detail,
356
- skills: [...new Set(found.at?.map((a) => a.skill))],
378
+ skills: [...new Set(found.at?.flatMap((a) => a.skill ? [a.skill] : []))],
357
379
  decision: !found.fix,
358
380
  fixes: (found.fix ? [found.fix] : found.choices ?? []).map((c) => fixOf(c, found))
359
381
  };
@@ -364,6 +386,9 @@ function filterOf(source) {
364
386
  function project(w, name) {
365
387
  return w.projects.find((p) => p.name === name);
366
388
  }
389
+ function usesKey(m) {
390
+ return m.source === "plugin" ? `${m.where.split("@")[0]}:${m.name}` : m.name;
391
+ }
367
392
  function totalUses(w, name) {
368
393
  return Object.values(w.usage).reduce((n, u) => n + (u[name] ?? 0), 0);
369
394
  }
@@ -383,7 +408,7 @@ function usable(w, projectName) {
383
408
  const uses = (n) => w.usage[projectName]?.[n] ?? 0;
384
409
  return [
385
410
  ...p.skills.map((s) => ({ name: s.name, source: s.source, where: s.dir, agents: s.agents, uses: uses(s.name), local: s })),
386
- ...machineIn(w, p).map((m) => ({ name: m.name, source: m.source, where: m.where, agents: m.agents, uses: uses(m.name), machine: m }))
411
+ ...machineIn(w, p).map((m) => ({ name: m.name, source: m.source, where: m.where, agents: m.agents, uses: uses(usesKey(m)), machine: m }))
387
412
  ];
388
413
  }
389
414
  function issuesAt(w, repo, skill) {
@@ -397,7 +422,7 @@ function repoIssues(w, repo) {
397
422
  function unlisted(w, repo) {
398
423
  const root = project(w, repo).path;
399
424
  const listed = new Set(usable(w, repo).map((u) => u.name));
400
- return [...new Set(w.issues.flatMap((i) => (i.at ?? []).filter((a) => a.root === root && !listed.has(a.skill)).map((a) => a.skill)))];
425
+ return [...new Set(w.issues.flatMap((i) => (i.at ?? []).filter((a) => a.root === root && a.skill && !listed.has(a.skill)).map((a) => a.skill)))];
401
426
  }
402
427
  function libraryIssues(w, name) {
403
428
  return w.issues.filter((i) => i.id === "unused:library" && i.at?.some((a) => a.skill === name)).map(issueOf);
@@ -410,12 +435,12 @@ function globalFixes(m) {
410
435
  return [
411
436
  {
412
437
  label: "Move it to the repos that need it\u2026",
413
- preview: `Pick repos. ${m.name} goes into your library and those repos, and stops loading globally (original backed up).`,
438
+ preview: `Pick repos. ${m.name} goes into your library and those repos, and stops loading globally (every global copy backed up).`,
414
439
  run: () => "",
415
440
  pickRepos: (w, repos) => w.ops.moveGlobal([m], repos)
416
441
  },
417
442
  m.kept ? { label: "Stop marking it as global on purpose", preview: `${m.name} shows as Not reviewed again, like any global skill.`, run: (w) => w.ops.keepGlobal(m.name, false) } : { label: "Keep it global on purpose", preview: `Mark ${m.name} as global on purpose: it no longer shows as Not reviewed. Its other issues stay.`, run: (w) => w.ops.keepGlobal(m.name, true) },
418
- { label: "Delete it", preview: `Remove ${m.path}. A backup is kept in Settings \u203A Backups.`, run: (w) => w.ops.deleteGlobal(m) }
443
+ { label: "Delete it", preview: `Remove ${m.where}/${m.name}. A backup is kept in Settings \u203A Backups.`, run: (w) => w.ops.deleteGlobal(m) }
419
444
  ];
420
445
  }
421
446
  function machineActions(m) {
@@ -466,7 +491,7 @@ function deleteLibraryFix(w, names) {
466
491
  `Delete ${what} and ${names.length === 1 ? "its" : "their"} versions from your library.`,
467
492
  repos.length ? `Removes ${names.length === 1 ? "it" : "them"} from ${repos.length === 1 ? repos[0] : `these ${repos.length} repos: ${repos.join(", ")}`}.` : "No repo uses it.",
468
493
  edited.length ? `${edited.join(", ")} ${edited.length === 1 ? "has" : "have"} local edits: backed up too.` : "",
469
- "Every copy goes to Settings \u203A Backups.",
494
+ "Every copy goes to Settings \u203A Backups; where a repo commits its copy, git will see it.",
470
495
  others.length ? `Untracked or the team's own copies in ${others.join(", ")} stay.` : ""
471
496
  ].filter(Boolean).join(" "),
472
497
  run: (w2) => joined(names.map((n) => w2.ops.deleteLibrary(n)))
@@ -495,7 +520,8 @@ function replacePluginFix(p) {
495
520
  `Copies its ${plural(names.length, "skill")} into your library and adds them to the repos you tick (or none),`,
496
521
  synced ? `then turns ${p.id} off in Claude Code. It's synced from your claude.ai account: to delete it for good, remove it there.` : p.settingsOnly ? `then turns ${p.id} off in ~/.claude/settings.json. It isn't installed, only turned on there, so there's nothing to uninstall.` : `then uninstalls ${p.id}${p.repo ? ` from ${p.repo}` : ""} (its saved data is kept).`,
497
522
  p.parts.length ? `It also brings ${p.parts.join(", ")}: those ${offOnly ? "stop" : "go"} too.` : "",
498
- offOnly ? "Turn it back on from Plugins." : "Settings \u203A Backups reinstalls it."
523
+ // A plugin only turned on in settings isn't listed in Plugins once it's off: say where it comes back.
524
+ offOnly ? p.settingsOnly ? "Turn it back on with /plugin in Claude Code." : "Turn it back on from Plugins." : "Settings \u203A Backups reinstalls it."
499
525
  ].filter(Boolean).join(" "),
500
526
  run: () => "",
501
527
  allowNone: true,
@@ -638,14 +664,16 @@ var agentNames = (agents) => agents.map((a) => harness(a).name).join(", ") || "n
638
664
  var localSource = (s) => s.source === "lib" ? "Your skills (skilllib library), installed in the repo" : s.source === "repo" ? "a repo's own skill (committed by the team)" : "a copy only on this machine (not committed)";
639
665
  var localInstalled = (s) => s.origin ?? (s.source === "lib" ? "added from my library with skilllib" : s.source === "repo" ? "committed in the repo" : "copied into the repo by hand (not committed)");
640
666
  function reviewPrompt(w, names, scope, repo) {
667
+ const project2 = repo ? w.projects.find((p) => p.name === repo) : void 0;
668
+ const machine = project2 ? machineIn(w, project2) : w.machine;
641
669
  const skills = names.map((name) => {
642
- const copies = w.machine.filter((m2) => m2.name === name);
643
- const here = repo ? w.projects.find((p) => p.name === repo)?.skills.find((s) => s.name === name) : void 0;
670
+ const copies = machine.filter((m2) => m2.name === name);
671
+ const here = project2?.skills.find((s) => s.name === name);
644
672
  if (here) {
645
673
  const lib2 = w.library.find((l) => l.name === name);
646
674
  return {
647
675
  name,
648
- description: w.descriptions[name] ?? "",
676
+ description: here.description ?? w.descriptions[name] ?? "",
649
677
  source: localSource(here),
650
678
  installedHow: localInstalled(here),
651
679
  path: here.path,
@@ -669,12 +697,13 @@ function reviewPrompt(w, names, scope, repo) {
669
697
  description: w.descriptions[name] ?? "",
670
698
  source: m ? m.source === "global" ? "my global skills" : `vendor: ${m.source} (${m.where})` : lib ? "Your skills (skilllib library)" : local ? localSource(local) : "a repo's own skill",
671
699
  installedHow: m?.origin ?? (m ? m.where : lib ? "created or imported into skilllib" : local ? localInstalled(local) : "committed in the repo"),
672
- path: m?.path ?? (lib ? w.ops.libraryFile(name).replace(/\/SKILL\.md$/, "") : local?.path ?? ""),
700
+ path: m?.path ?? (lib ? dirname(w.ops.libraryFile(name)) : local?.path ?? ""),
673
701
  links: m?.links ?? [],
674
- loadedBy: m ? `${agentNames(m.agents)} \u2014 in every repo` : repos.map((p) => `${agentNames(p.skills.find((s) => s.name === name).agents)} \u2014 in ${p.name}`).join("; ") || "none of my agents",
702
+ loadedBy: m ? `${agentNames(m.agents)} \u2014 ${w.machine.some((g) => g.path === m.path) ? "in every repo" : `in ${repo}`}` : repos.map((p) => `${agentNames(p.skills.find((s) => s.name === name).agents)} \u2014 in ${p.name}`).join("; ") || "none of my agents",
675
703
  vendor: m && m.source !== "global" ? "turn it off at its source (/plugin in Claude Code, claude.ai settings, or Cursor)" : null,
676
- usesTotal: w.days ? totalUses(w, name) : null,
677
- usesByProject: Object.entries(w.usage).flatMap(([p, u]) => u[name] ? [[p, u[name]]] : []),
704
+ // A plugin's copy is counted under plugin:skill.
705
+ usesTotal: w.days ? totalUses(w, m ? usesKey(m) : name) : null,
706
+ usesByProject: Object.entries(w.usage).flatMap(([p, u]) => /* @__PURE__ */ ((n) => n ? [[p, n]] : [])(u[m ? usesKey(m) : name])),
678
707
  installedIn: repos.map((p) => p.name),
679
708
  otherCopies: copies.filter((c) => c !== m).map((c) => `${c.source} ${c.where}`),
680
709
  inYourSkills: lib ? `yes, v${lib.latest}` : null,
@@ -690,7 +719,7 @@ function reviewPrompt(w, names, scope, repo) {
690
719
  }
691
720
  function writeSkillPrompt(w, name, repo) {
692
721
  const p = repo ? w.projects.find((x) => x.name === repo) : void 0;
693
- const file = p ? `${p.path}/.claude/skills/${name || "<name>"}/SKILL.md` : w.ops.libraryFile(name || "<name>");
722
+ const file = p ? `${p.path}/${installDirs(w.agents)[0]}/${name || "<name>"}/SKILL.md` : w.ops.libraryFile(name || "<name>");
694
723
  return `Write a new agent skill${name ? ` named "${name}"` : ""}${p ? ` for the repo at ${p.path}` : " for my skill library"}.
695
724
 
696
725
  Create ${file} with:
@@ -739,7 +768,7 @@ function tag(u) {
739
768
  return [u.source, color.muted];
740
769
  }
741
770
  function isNameChar(input, key) {
742
- return !key.ctrl && !key.meta && /^[a-zA-Z0-9._-]$/.test(input);
771
+ return !key.ctrl && !key.meta && /^[a-zA-Z0-9._-]+$/.test(input);
743
772
  }
744
773
  function clip(text, width) {
745
774
  return text.length > width - 1 ? text.slice(0, width - 2) + "\u2026" : text;
@@ -755,7 +784,7 @@ function builder(title) {
755
784
  issues(issues) {
756
785
  if (!issues.length) return d.body.push({ segs: [["\u2713 No issues", color.green]] });
757
786
  for (const issue of [...issues].sort(bySeverity)) {
758
- d.body.push({ segs: [[`${SEV[issue.severity].icon} `, SEV[issue.severity].color], [issue.title, color.text], [issue.decision ? " \xB7 your call" : "", color.faint]] });
787
+ d.body.push({ segs: [[`${SEV[issue.severity].icon} `, SEV[issue.severity].color], [issue.title, color.text], [issue.decision && issue.fixes.length ? " \xB7 your call" : "", color.faint]] });
759
788
  issue.fixes.forEach((fix, i) => d.body.push({ segs: [], option: d.options.push({ label: fix.label, fix, issue, recommended: !issue.decision && i === 0 }) - 1 }));
760
789
  }
761
790
  return 0;
@@ -848,13 +877,14 @@ function skillItem(w, copies, issues, nameW, actions, repo) {
848
877
  ],
849
878
  detail: (width) => {
850
879
  const b = builder(u.name);
851
- for (const l of wrap(w.descriptions[u.name] || "(no description)", width - 4, 3)) b.line([l, color.muted]);
880
+ for (const l of wrap(u.local?.description || w.descriptions[u.name] || "(no description)", width - 4, 3)) b.line([l, color.muted]);
852
881
  b.line();
853
- if (repo && !u.local) b.line(["Loads in every repo", color.text], [u.source === "global" || u.source === "skilllib" ? " issues and cleanup live in Global" : " managed at its source", color.faint]);
882
+ const here = !!u.machine && !w.machine.some((m) => m.path === u.machine.path && m.where === u.machine.where);
883
+ if (repo && !u.local) b.line([here ? "Loads in this repo (its settings turn it on)" : "Loads in every repo", color.text], [u.source === "global" || u.source === "skilllib" ? " issues and cleanup live in Global" : " managed at its source", color.faint]);
854
884
  else b.issues(issues);
855
885
  b.actions(actions, width);
856
886
  b.line();
857
- for (const segs of usageLines(w, u.name, width, repo)) b.line(...segs);
887
+ for (const segs of usageLines(w, u.machine ? usesKey(u.machine) : u.name, width, repo)) b.line(...segs);
858
888
  b.line();
859
889
  b.line(["Loaded from", color.accent]);
860
890
  for (const c of copies) {
@@ -904,12 +934,13 @@ var reviewOption = (w, name, ui, repo) => ({
904
934
  label: "Review prompt",
905
935
  action: () => ui.copy(reviewPrompt(w, [name], `Review the skill ${name}${repo ? ` in ${repo}` : ""}.`, repo), `a review prompt for ${name}`)
906
936
  });
937
+ var gitSees = (s) => isShared(s?.git) ? " Git will see it." : "";
907
938
  function versionsOption(w, repo, name, installed, ui) {
908
939
  return {
909
940
  label: "Other versions\u2026",
910
941
  action: () => ui.menu(
911
942
  `${name}: versions`,
912
- w.ops.versions(name).reverse().map((v) => fixOption({ label: `v${v.version} ${v.date}${v.version === installed ? " (installed)" : ""}`, preview: `Install ${name} v${v.version} in ${repo}.`, run: (w2) => w2.ops.installVersion(repo, name, v.version) }))
943
+ w.ops.versions(name).reverse().map((v) => fixOption({ label: `v${v.version} ${v.date}${v.version === installed ? " (installed)" : ""}`, preview: `Install ${name} v${v.version} in ${repo}.${gitSees(project(w, repo).skills.find((x) => x.name === name))}`, run: (w2) => w2.ops.installVersion(repo, name, v.version) }))
913
944
  )
914
945
  };
915
946
  }
@@ -919,14 +950,15 @@ function skillActions(w, p, u, ui) {
919
950
  const remove = {
920
951
  // Its folder gone: all that's left to remove is the skilllib.json entry (the label Health offers too).
921
952
  label: s?.missing ? "Remove it from skilllib.json" : "Remove from repo",
922
- preview: (s?.missing ? `${p}'s skilllib.json stops pinning ${u.name}. There's no folder to delete.` : s?.source === "lib" ? `Delete ${s.dir}/${u.name} in ${p}.${inLibrary ? " Your library keeps it." : ""}` : `Move ${s?.dir}/${u.name} in ${p} to Settings \u203A Backups.`) + (s?.others ? ` The copy in ${s.others.join(", ")} stays.` : ""),
953
+ preview: (s?.missing ? `${p}'s skilllib.json stops pinning ${u.name}. There's no folder to delete.` : s?.source === "lib" ? `Delete ${s.dir}/${u.name} in ${p}.${inLibrary ? " Your library keeps it." : ""}` : `Move ${s?.dir}/${u.name} in ${p} to Settings \u203A Backups.`) + (s?.others ? ` The copy in ${s.others.join(", ")} stays.` : "") + // A managed skill's removal edits skilllib.json too.
954
+ (gitSees(s) || (s?.source === "lib" && project(w, p).manifest === "committed" ? " Git will see skilllib.json change." : "")),
923
955
  run: (w2) => w2.ops.remove(p, u.name)
924
956
  };
925
957
  if (s?.source === "lib")
926
958
  return [
927
959
  // A tracked skill your library doesn't have: nothing to edit there, no versions to pick.
928
960
  ...inLibrary ? [
929
- ...s.version < inLibrary.latest ? [fixOption({ label: `Update to v${inLibrary.latest}`, preview: `Replace ${s.dir}/${u.name} with library v${inLibrary.latest}.`, run: (w2) => w2.ops.update(p, u.name) })] : [],
961
+ ...s.version < inLibrary.latest ? [fixOption({ label: `Update to v${inLibrary.latest}`, preview: `Replace ${s.dir}/${u.name} with library v${inLibrary.latest}.${gitSees(s)}`, run: (w2) => w2.ops.update(p, u.name) })] : [],
930
962
  { label: "Edit in library", action: () => ui.edit(w.ops.libraryFile(u.name)) },
931
963
  versionsOption(w, p, u.name, s.version, ui)
932
964
  ] : [],
@@ -945,7 +977,8 @@ function skillActions(w, p, u, ui) {
945
977
  fixOption(remove),
946
978
  reviewOption(w, u.name, ui, p)
947
979
  ];
948
- return [{ label: "Open in Global", action: () => ui.openInGlobal(u.name) }, reviewOption(w, u.name, ui)];
980
+ const everywhere = !!u.machine && w.machine.some((m) => m.path === u.machine.path && m.where === u.machine.where);
981
+ return everywhere ? [{ label: "Open in Global", action: () => ui.openInGlobal(u.name) }, reviewOption(w, u.name, ui)] : [reviewOption(w, u.name, ui, p)];
949
982
  }
950
983
  function overview(w, p) {
951
984
  const project2 = project(w, p);
@@ -992,7 +1025,7 @@ function projectItems(w) {
992
1025
  });
993
1026
  }
994
1027
  function globalItems(w, nameW, ui) {
995
- const copies = w.machine.map((m) => ({ name: m.name, source: m.source, where: m.where, agents: m.agents, uses: totalUses(w, m.name), machine: m }));
1028
+ const copies = w.machine.map((m) => ({ name: m.name, source: m.source, where: m.where, agents: m.agents, uses: totalUses(w, usesKey(m)), machine: m }));
996
1029
  return byName(copies).map((g) => skillItem(w, g, issuesAt(w, null, g[0].name), nameW, globalActions(w, g[0], ui), null));
997
1030
  }
998
1031
  var AT_SOURCE = {
@@ -1055,7 +1088,7 @@ function libraryItems(w, nameW, ui) {
1055
1088
  ...removable.length ? [
1056
1089
  fixOption({
1057
1090
  label: "Remove from repos\u2026",
1058
- preview: `Pick repos to remove ${l.name} from. Your library keeps it.`,
1091
+ preview: `Pick repos to remove ${l.name} from. Your library keeps it. Where a repo commits its copy, git will see it.`,
1059
1092
  run: () => "",
1060
1093
  candidates: () => removable.map((p) => p.name),
1061
1094
  pickRepos: (w2, rs) => joined(rs.map((r) => w2.ops.remove(r, l.name)))
@@ -1149,7 +1182,7 @@ function pluginItems(w, columns, skillW, ui, issuesOf) {
1149
1182
  const skills = p.skills.map((s) => cursor || !w.days ? s.name : `${s.name} (${uses2(totalUses(w, pluginSkillKey(p, s.name)))})`);
1150
1183
  wrap(skills.join(" \xB7 ") || "none", width - 16, 4).forEach((l, i) => b.line(label(i ? "" : "Skills"), [l, color.text]));
1151
1184
  b.line(label("Also brings"), [p.parts.join(", ") || "nothing else", p.parts.length ? color.text : color.faint]);
1152
- const installed = cursor ? "in Cursor (whether it's on is only in Cursor)" : p.scope === "claude.ai" ? "synced from your claude.ai account" : p.repo ? `${p.scope} scope, in ${p.repo} only` : `user scope, in every repo`;
1185
+ const installed = cursor ? "in Cursor (whether it's on is only in Cursor)" : p.scope === "claude.ai" ? "synced from your claude.ai account" : p.settingsOnly ? "not installed: turned on in your settings, loaded from its marketplace's copy" : p.repo ? `${p.scope} scope, in ${p.repo} only` : `user scope, in every repo`;
1153
1186
  b.line(label("Installed"), [installed, color.text], [p.version ? ` v${p.version.replace(/^v/, "")}` : "", color.muted], [p.id.includes("@") && p.scope !== "claude.ai" ? ` from ${p.id.split("@")[1]}` : "", color.faint]);
1154
1187
  b.line(label("Folder"), [tildify(p.path), color.faint]);
1155
1188
  return b.d;
@@ -1174,7 +1207,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1174
1207
  const world = { ...withUsage, issues: [...base.issues, ...usageIssues2] };
1175
1208
  useEffect(() => {
1176
1209
  let live = true;
1177
- void loadUsage2?.(base).then((u) => live && setUsed(u));
1210
+ void loadUsage2?.(base).then((u) => live && setUsed(u)).catch(() => live && setToast("\u2717 Couldn't read your Claude Code transcripts: usage isn't shown"));
1178
1211
  return () => {
1179
1212
  live = false;
1180
1213
  };
@@ -1253,7 +1286,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1253
1286
  }
1254
1287
  function choose(o) {
1255
1288
  if (o.action) return o.action();
1256
- if (!o.fix) return setToast("Not in the prototype");
1289
+ if (!o.fix) return;
1257
1290
  if (o.fix.pickRepos) {
1258
1291
  const name = current?.name ?? "";
1259
1292
  const only = o.fix.candidates?.(world);
@@ -1319,7 +1352,8 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1319
1352
  cells: [
1320
1353
  { text: top ? SEV[top.severity].icon : " ", width: 2, color: top ? SEV[top.severity].color : void 0 },
1321
1354
  { text: `${isOpen ? "\u25BE" : "\u25B8"} ${label} (${members.length})`, grow: true, color: color.accent },
1322
- { text: flagged ? `${flagged} to fix` : "", width: 10, color: top ? SEV[top.severity].color : color.faint }
1355
+ // A leading space keeps it apart from a name cut to fit.
1356
+ { text: flagged ? ` ${flagged} to fix` : "", width: 11, color: top ? SEV[top.severity].color : color.faint }
1323
1357
  ],
1324
1358
  detail: (width) => {
1325
1359
  const b = builder(label);
@@ -1398,7 +1432,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1398
1432
  () => ui.menu(repo, [
1399
1433
  { label: "Open its folder", action: () => setToast(said2(world.ops.openFolder(project(world, repo).path))) },
1400
1434
  { label: "Hide it from the list", action: () => (apply((w) => w.ops.hide(repo)), setOpen(null), setFocus(sidebar ? "sidebar" : "list")) },
1401
- ...skills.length ? [{ label: `Review prompt for its ${skills.length} skills`, action: () => ui.copy(reviewPrompt(world, skills, `Review the skills in ${repo}.`, repo), `a review prompt for ${repo}`) }] : []
1435
+ ...skills.length ? [{ label: `Review prompt for its ${skills.length === 1 ? "skill" : `${skills.length} skills`}`, action: () => ui.copy(reviewPrompt(world, skills, `Review the skills in ${repo}.`, repo), `a review prompt for ${repo}`) }] : []
1402
1436
  ])
1403
1437
  )
1404
1438
  ];
@@ -1446,8 +1480,16 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1446
1480
  setToast(`Saved ${tildify(file)}`);
1447
1481
  });
1448
1482
  }
1449
- function askForFolder(title) {
1450
- setModal({ kind: "input", title, value: "", placeholder: "~/Projects", submit: (v) => apply((w) => w.ops.addRoot(v)) });
1483
+ function askForFolder(title, problem) {
1484
+ const submit = (v) => apply((w) => {
1485
+ try {
1486
+ return w.ops.addRoot(v);
1487
+ } catch (e) {
1488
+ askForFolder(title, e.message);
1489
+ throw e;
1490
+ }
1491
+ });
1492
+ setModal({ kind: "input", title: problem ? `${problem}. ${title}` : title, value: "", placeholder: "~/Projects", submit });
1451
1493
  }
1452
1494
  useEffect(() => {
1453
1495
  const folder = () => !initial.roots.length && askForFolder("Where are your projects? skilllib finds the git repos in there");
@@ -1701,7 +1743,8 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1701
1743
  if (isNameChar(input, key)) return setModal({ ...m, query: m.query + input.toLowerCase(), cursor: 0 });
1702
1744
  }
1703
1745
  }
1704
- const bottomH = 1;
1746
+ const toastLines = toast && modal?.kind !== "confirm" ? wrap(toast, termCols - 2, 3).map((l) => " " + l) : [];
1747
+ const bottomH = Math.max(1, toastLines.length);
1705
1748
  const confirmLines = modal?.kind === "confirm" ? wrap(modal.fix.preview, columns - 6, 6) : [];
1706
1749
  const confirmH = modal?.kind === "confirm" ? confirmLines.length + 2 : 0;
1707
1750
  const bodyH = Math.max(8, termRows - (sidebar ? 0 : 1) - bottomH - confirmH);
@@ -1952,9 +1995,9 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1952
1995
  modal?.kind === "confirm" ? /* @__PURE__ */ jsx2(Panel, { title: modal.fix.label, focused: true, width: columns, height: confirmH, children: confirmLines.map((l, i) => /* @__PURE__ */ jsx2(Text2, { color: color.text, children: l }, i)) }) : null
1953
1996
  ] })
1954
1997
  ] }),
1955
- /* @__PURE__ */ jsxs2(Box2, { height: 1, width: termCols, justifyContent: "space-between", children: [
1956
- /* @__PURE__ */ jsx2(Text2, { color: toast.startsWith("\u2713") ? color.green : toast.startsWith("\u2717") ? color.red : toast.startsWith("!") ? color.yellow : color.muted, wrap: "truncate-end", children: " " + toast }),
1957
- toast ? null : /* @__PURE__ */ jsx2(Text2, { color: color.faint, children: hints + " " })
1998
+ /* @__PURE__ */ jsxs2(Box2, { height: bottomH, width: termCols, justifyContent: "space-between", children: [
1999
+ toastLines.length ? /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: toastLines.map((l, i) => /* @__PURE__ */ jsx2(Text2, { wrap: "truncate-end", color: toast.startsWith("\u2713") ? color.green : toast.startsWith("\u2717") ? color.red : toast.startsWith("!") ? color.yellow : color.muted, children: l }, i)) }) : /* @__PURE__ */ jsx2(Text2, { color: toast.startsWith("\u2713") ? color.green : toast.startsWith("\u2717") ? color.red : toast.startsWith("!") ? color.yellow : color.muted, wrap: "truncate-end", children: " " + toast }),
2000
+ toast && modal?.kind !== "confirm" ? null : /* @__PURE__ */ jsx2(Text2, { color: color.faint, children: " " + hints + " " })
1958
2001
  ] })
1959
2002
  ] });
1960
2003
  }
@@ -1999,7 +2042,7 @@ function Help({ width, height }) {
1999
2042
  const notes = [
2000
2043
  '\u2715 problem \xB7 \u26A0 suggestion \xB7 \u2605 recommended fix \xB7 "your call" = a decision, never auto-fixed',
2001
2044
  "Source: lib = from your library \xB7 repo = committed by the team \xB7 untracked = only on this machine",
2002
- "Usage counts Claude Code sessions only: other agents don't record skill use."
2045
+ "Usage counts Claude Code sessions only: skilllib reads no other agent's transcripts."
2003
2046
  ];
2004
2047
  const lines = [
2005
2048
  ...keys.flatMap(([k, d]) => wrap(d, width - 16, 3).map((l, i) => [[(i ? "" : k).padEnd(12), color.accent], [l, color.text]])),
@@ -2010,12 +2053,9 @@ function Help({ width, height }) {
2010
2053
  }
2011
2054
 
2012
2055
  // src/tui/load.ts
2013
- import { spawn } from "child_process";
2014
- import { existsSync as existsSync3, lstatSync, realpathSync } from "fs";
2015
- import { basename, dirname, join as join2 } from "path";
2016
- function tildify2(p) {
2017
- return tildify(p).replace(/\\/g, "/");
2018
- }
2056
+ import { spawnSync as spawnSync3 } from "child_process";
2057
+ import { existsSync as existsSync3, lstatSync } from "fs";
2058
+ import { basename, dirname as dirname2, join as join2, relative, sep } from "path";
2019
2059
  function uniqueNames(roots) {
2020
2060
  const tail = (r, n) => r.split(/[\\/]/).filter(Boolean).slice(-n).join("/");
2021
2061
  return new Map(
@@ -2044,7 +2084,7 @@ function localSkill(root, s, git) {
2044
2084
  };
2045
2085
  const state = git ? git.of(relativeTo(root, s.path)) : null;
2046
2086
  const others = otherCopies(root, s.name, s.location);
2047
- 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" } : {};
2048
2088
  return {
2049
2089
  name: s.name,
2050
2090
  source: "lib",
@@ -2059,7 +2099,8 @@ function localSkill(root, s, git) {
2059
2099
  };
2060
2100
  }
2061
2101
  function otherCopies(root, name, dir) {
2062
- return foldersOf(root, name).real.map((d) => relativeTo(root, d)).filter((d) => d !== dir);
2102
+ const self = realPath(join2(root, dir, name));
2103
+ return foldersOf(root, name).real.filter((d) => realPath(join2(d, name)) !== self).map((d) => relativeTo(root, d));
2063
2104
  }
2064
2105
  function installedAt(path) {
2065
2106
  try {
@@ -2073,8 +2114,8 @@ function machineSkill(s, kept) {
2073
2114
  const global = s.kind === "global" || s.kind === "skills.sh";
2074
2115
  return {
2075
2116
  name: s.name,
2076
- source: global ? "global" : s.kind === "plugin" ? "plugin" : s.kind === "claude.ai" ? "claude.ai" : s.kind === "system" ? "system" : s.kind === "skilllib" ? "skilllib" : s.kind === "built-in" && s.harnesses[0] === "grok" ? "grok" : "cursor",
2077
- where: global || s.kind === "skilllib" ? tildify2(dirname(s.path)) : s.kind === "claude.ai" ? "claude.ai account" : s.kind === "built-in" ? `${s.origin} built-in` : s.origin,
2117
+ source: global ? "global" : s.kind === "plugin" ? "plugin" : s.kind === "claude.ai" ? "claude.ai" : s.kind === "system" || s.kind === "built-in" && s.harnesses[0] === "codex" ? "system" : s.kind === "skilllib" ? "skilllib" : s.kind === "built-in" && s.harnesses[0] === "grok" ? "grok" : "cursor",
2118
+ where: global || s.kind === "skilllib" ? tildify(dirname2(s.path)) : s.kind === "claude.ai" ? "claude.ai account" : s.kind === "built-in" ? `${s.origin} built-in` : s.origin,
2078
2119
  ...s.kind === "skills.sh" ? { origin: `npx skills: ${s.origin}` } : {},
2079
2120
  path: s.path,
2080
2121
  links: s.links,
@@ -2111,13 +2152,8 @@ function loadWorld() {
2111
2152
  forgetClaudeBinary();
2112
2153
  const agents = enabledHarnesses();
2113
2154
  const here = projectHere();
2114
- const real = (p) => {
2115
- try {
2116
- return realpathSync(p);
2117
- } catch {
2118
- return p;
2119
- }
2120
- };
2155
+ if (here) rememberProjects([here]);
2156
+ const real = realPath;
2121
2157
  const visible = visibleProjects();
2122
2158
  const cwd = here ? visible.find((p) => real(p) === real(here)) ?? here : null;
2123
2159
  const listed = [.../* @__PURE__ */ new Set([...cwd ? [cwd] : [], ...visible])];
@@ -2132,7 +2168,7 @@ function loadWorld() {
2132
2168
  const libraryNames = new Set(library.map((l) => l.name));
2133
2169
  const kept = keptGlobal();
2134
2170
  const cursorList = agents.includes("cursor") ? cursorPlugins() : [];
2135
- const sourced = machineSkills(agents);
2171
+ const sourced = machineSkills(agents).filter((s) => s.harnesses.length || s.broken);
2136
2172
  const machine = sourced.map((s) => {
2137
2173
  descriptions[s.name] ??= s.description;
2138
2174
  return machineSkill(s, kept);
@@ -2153,7 +2189,11 @@ function loadWorld() {
2153
2189
  name: names.get(root),
2154
2190
  path: root,
2155
2191
  manifest: hasManifest ? git ? git.of("skilllib.json") : "no git" : "none",
2156
- skills: status.map((s) => localSkill(root, s, git)),
2192
+ skills: status.map((s) => {
2193
+ const local = localSkill(root, s, git);
2194
+ const own = s.state.includes("missing") ? "" : readSkillInfo(s.path).description;
2195
+ return own && own !== descriptions[s.name] ? { ...local, description: own } : local;
2196
+ }),
2157
2197
  ...loadedHere(root)
2158
2198
  };
2159
2199
  });
@@ -2169,7 +2209,7 @@ function loadWorld() {
2169
2209
  library,
2170
2210
  usage: {},
2171
2211
  descriptions,
2172
- backups: backups.map((b) => ({ name: b.name, from: tildify2(b.from), at: b.movedAt })),
2212
+ backups: backups.map((b) => ({ name: b.name, from: tildify(b.from), at: b.movedAt })),
2173
2213
  ...(({ roots: roots2, hidden }) => ({ roots: roots2, hidden }))(readConfig()),
2174
2214
  agentsChosen: harnessesChosen(),
2175
2215
  agentSkill: agentSkillState(agents),
@@ -2186,7 +2226,7 @@ async function loadUsage(w) {
2186
2226
  const today = midnight(Date.now());
2187
2227
  const totals = {};
2188
2228
  for (const use of await scanUsage(30)) {
2189
- const skill = use.skill.split(":").pop();
2229
+ const skill = skillOf(use.skill);
2190
2230
  totals[skill] = (totals[skill] ?? 0) + 1;
2191
2231
  if (skill !== use.skill) totals[use.skill] = (totals[use.skill] ?? 0) + 1;
2192
2232
  const root = projectOf(use.cwd);
@@ -2200,22 +2240,26 @@ async function loadUsage(w) {
2200
2240
  }
2201
2241
  function said3(c, repo) {
2202
2242
  if (c.action === "skipped") return `${c.name}: ${c.reason ?? "skipped"}`;
2203
- if (c.action === "removed") return `Removed ${c.name} from ${repo}`;
2243
+ if (c.action === "removed") return `Removed ${c.name} from ${repo}${c.reason ? ` (${c.reason})` : ""}`;
2244
+ if (c.action === "unchanged") return `${c.name}${c.to ? ` v${c.to}` : ""} is already in ${repo}`;
2245
+ if (c.action === "relinked") return `Linked ${c.name}${c.to ? ` v${c.to}` : ""} for your other agents in ${repo}`;
2204
2246
  const back = c.action === "updated" && c.from !== void 0 && c.to !== void 0 && c.to < c.from;
2205
2247
  const done = back ? `Pinned ${c.name} to v${c.to} in ${repo} (was v${c.from})` : `${c.action === "installed" ? "Added" : c.action === "reset" ? "Reset" : "Updated"} ${c.name}${c.to ? ` v${c.to}` : ""} in ${repo}`;
2206
2248
  return c.backedUp ? `${done} (your edits are in Settings \u203A Backups)` : done;
2207
2249
  }
2208
2250
  function realOps(roots, backups, machine, libraryNames) {
2209
2251
  const rootOf = (repo) => roots.get(repo);
2252
+ const nameOf = (root) => [...roots].find(([, r]) => r === root)?.[0] ?? basename(root);
2210
2253
  const find = (repo, name) => projectStatus(rootOf(repo)).find((s) => s.name === name);
2211
2254
  const result = (c, repo) => c.action === "skipped" ? failed(said3(c, repo)) : said3(c, repo);
2212
2255
  const copyDir = (repo, name) => readManifest(rootOf(repo)).skills[name]?.dir ?? PROJECT_SKILLS_DIR;
2213
2256
  const withLinks = (c, repo, name) => {
2214
2257
  const it = (dirs) => dirs.length === 1 ? "it" : "them";
2258
+ const way = c.inTheWay?.length ? `; not linked everywhere: ${inTheWaySaid(rootOf(repo), c.inTheWay)} (clear the way, then link again)` : "";
2215
2259
  if (c.uncommitted?.length)
2216
- return `${said3(c, repo)}; not linked in ${c.uncommitted.join(", ")}: git tracks ${it(c.uncommitted)} and ${copyDir(repo, name)}/${name} isn't committed, so teammates would get broken links`;
2217
- if (!c.blocked?.length) return result(c, repo);
2218
- return { message: `${said3(c, repo)}; not linked in ${c.blocked.join(", ")}: git tracks ${it(c.blocked)}`, then: allowTracked([{ repo, name, blocked: c.blocked }]) };
2260
+ return `${said3(c, repo)}${way}; not linked in ${c.uncommitted.join(", ")}: git tracks ${it(c.uncommitted)} and ${copyDir(repo, name)}/${name} isn't committed, so teammates would get broken links`;
2261
+ if (!c.blocked?.length) return way ? failed(`${said3(c, repo)}${way}`) : result(c, repo);
2262
+ return { message: `${said3(c, repo)}${way}; not linked in ${c.blocked.join(", ")}: git tracks ${it(c.blocked)}`, then: allowTracked([{ repo, name, blocked: c.blocked }]) };
2219
2263
  };
2220
2264
  const addTo = (repos, names) => {
2221
2265
  if (repos.length === 1 && names.length === 1) return withLinks(addSkill(rootOf(repos[0]), names[0]), repos[0], names[0]);
@@ -2237,10 +2281,11 @@ function realOps(roots, backups, machine, libraryNames) {
2237
2281
  const why = kind === "blocked" ? `git tracks ${dirs.length === 1 ? "it" : "them"}` : `${copies.join(", ")} ${copies.length === 1 ? "isn't" : "aren't"} committed, so a link would break for teammates`;
2238
2282
  notes.push(`${who}not linked in ${dirs.join(", ")} in ${repo}: ${why}`);
2239
2283
  }
2284
+ for (const x of done.filter((d) => d.c.inTheWay?.length)) notes.push(`${x.name} not linked everywhere in ${x.repo}: ${inTheWaySaid(rootOf(x.repo), x.c.inTheWay)}`);
2240
2285
  const skipped = changes.filter((x) => x.c.action === "skipped").map((x) => `${x.name} not added to ${x.repo}: ${x.c.reason}`);
2241
2286
  const text = [summary, ...notes, ...skipped].filter(Boolean).join("; ");
2242
2287
  const held = done.flatMap((x) => x.c.blocked?.length ? [{ repo: x.repo, name: x.name, blocked: x.c.blocked }] : []);
2243
- const result2 = !done.length ? failed(text) : held.length ? { message: text, then: allowTracked(held) } : skipped.length ? failed(text) : text;
2288
+ const result2 = !done.length ? failed(text) : held.length ? { message: text, then: allowTracked(held) } : skipped.length || done.some((x) => x.c.inTheWay?.length) ? failed(text) : text;
2244
2289
  return { result: result2, skipped, notes };
2245
2290
  };
2246
2291
  const allowTracked = (held) => {
@@ -2255,24 +2300,29 @@ function realOps(roots, backups, machine, libraryNames) {
2255
2300
  const linked = results.filter((x) => x.r.created.length);
2256
2301
  const names = [...new Set(linked.map((x) => x.name))];
2257
2302
  const summary = linked.length ? `Linked ${names.length === 1 ? names[0] : `${names.length} skills`} in ${[...new Set(linked.flatMap((x) => x.r.created))].join(", ")} in ${[...new Set(linked.map((x) => x.repo))].join(", ")}` : "";
2258
- const others = results.filter((x) => !x.r.created.length || x.r.uncommitted.length).map((x) => `${x.repo}: ${linkSaid(x.name, x.r)}`);
2303
+ const others = results.filter((x) => !x.r.created.length || x.r.uncommitted.length || x.r.inTheWay.length).map((x) => `${x.repo}: ${linkSaid(x.repo, x.name, x.r)}`);
2259
2304
  return [summary, ...others].join("; ");
2260
2305
  }
2261
2306
  };
2262
2307
  };
2263
2308
  const linkIn = (repo, name, allow) => {
2264
2309
  const s = find(repo, name);
2310
+ if (!s) throw new Error(`${name} isn't in ${repo} any more`);
2265
2311
  return s.managed ? relinkDependency(rootOf(repo), name, { allowTracked: allow }) : linkEverywhere(rootOf(repo), name, s.location, { allowTracked: allow });
2266
2312
  };
2267
- const linkSaid = (name, r) => {
2313
+ const linkSaid = (repo, name, r) => {
2268
2314
  const linked = r.created.length ? `linked in ${r.created.join(", ")}` : "";
2269
2315
  const notLinked = r.uncommitted.length ? `not linked in ${r.uncommitted.join(", ")}: git tracks ${r.uncommitted.length === 1 ? "it" : "them"} and ${name} isn't committed, so teammates would get broken links` : "";
2270
- return `${name}: ${[linked, notLinked].filter(Boolean).join("; ") || "nothing to link"}`;
2316
+ const inTheWay = r.inTheWay.length ? inTheWaySaid(rootOf(repo), r.inTheWay) : "";
2317
+ return `${name}: ${[linked, notLinked, inTheWay].filter(Boolean).join("; ") || "nothing to link"}`;
2271
2318
  };
2272
- const orForce = (c, repo, label, force) => c.action === "skipped" && c.reason?.includes("edits") ? {
2273
- message: `${c.name} in ${repo} has local edits`,
2274
- then: { label, preview: `Your edits to ${c.name} in ${repo} go to Settings \u203A Backups.`, run: () => result(force(), repo) }
2275
- } : result(c, repo);
2319
+ const orForce = (c, repo, label, force) => (
2320
+ // Whatever --force would get past (edits, a pin from another library, another copy in the way): offer it here.
2321
+ c.action === "skipped" && !!c.reason?.includes("--force") ? {
2322
+ message: `${c.name} in ${repo}: ${withoutForceHint(c.reason)}`,
2323
+ then: { label, preview: `What's there now in ${repo} goes to Settings \u203A Backups first.`, run: () => result(force(), repo) }
2324
+ } : result(c, repo)
2325
+ );
2276
2326
  const toLibrary = (path) => {
2277
2327
  const r = importSkill(path, { force: true });
2278
2328
  return r.status === "unchanged" ? `${r.name} is already the same in your library` : `${r.name} v${latestVersion(r.name)?.version} is in your library`;
@@ -2283,7 +2333,7 @@ function realOps(roots, backups, machine, libraryNames) {
2283
2333
  remove: (repo, name) => {
2284
2334
  const s = find(repo, name);
2285
2335
  if (!s) return failed(`${name} isn't in ${repo}`);
2286
- 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)));
2287
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`);
2288
2338
  const others = otherCopies(rootOf(repo), name, s.location);
2289
2339
  const left = (r) => typeof r === "string" && others.length ? `${r}; the copy in ${others.join(", ")} stays` : r;
@@ -2294,50 +2344,66 @@ function realOps(roots, backups, machine, libraryNames) {
2294
2344
  const c = updateProject(rootOf(repo), [name])[0];
2295
2345
  return c ? orForce(c, repo, "Update anyway", () => updateProject(rootOf(repo), [name], { force: true })[0]) : `${name} is up to date`;
2296
2346
  },
2297
- 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)),
2298
2348
  deleteGlobal: (m) => {
2299
- const r = deleteGlobal(m.path, m.links);
2300
- return r.ok ? m.broken ? `Removed the broken link ${m.name}` : `Deleted ${m.name} (in Settings \u203A Backups)` : failed(`${m.name}: ${r.reason}`);
2349
+ if (!entryExists(m.path)) return `${tildify(m.path)} is already gone`;
2350
+ const others = machine.filter((o) => o.name === m.name && o.path !== m.path && o.kind !== "plugin" && !o.broken);
2351
+ const dependents = others.filter((o) => !survives(o.path, [m.path, ...m.links]));
2352
+ const stays = others.filter((o) => !dependents.includes(o));
2353
+ if (!m.broken) {
2354
+ const readers = [.../* @__PURE__ */ new Set([...m.agents, ...dependents.flatMap((o) => o.harnesses)])].filter((id) => !stays.some((o) => o.harnesses.includes(id)));
2355
+ const blind = blindWithoutGlobal(m.name, [{ path: m.path, harnesses: readers, links: [...m.links, ...dependents.map((o) => o.path)] }], [...roots.values(), ...knownProjects()]);
2356
+ if (blind.length) return failed(`${m.name} kept: ${blindSaid(m.name, blind, nameOf)}`);
2357
+ }
2358
+ const left = m.broken ? [] : strandedLinks(m.name, [{ path: m.path, links: [...m.links, ...dependents.map((o) => o.path)] }], [...roots.values(), ...knownProjects()], nameOf);
2359
+ const r = deleteGlobal(m.path, [...m.links, ...dependents.map((o) => o.path)]);
2360
+ if (!r.ok) return failed(`${m.name}: ${r.reason}`);
2361
+ if (m.broken) return `Removed the broken link ${m.name}`;
2362
+ const still = stays.map((o) => tildify(o.path));
2363
+ return `Deleted ${tildify(m.path)} (in Settings \u203A Backups)${still.length ? `; ${m.name} still loads globally from ${still.join(", ")}` : ""}${left.length ? `; ${left.join(", ")} now point${left.length === 1 ? "s" : ""} at nothing: remove ${left.length === 1 ? "it" : "them"}` : ""}`;
2301
2364
  },
2302
2365
  keepGlobal: (name, keep) => (setKeepGlobal([name], keep), keep ? `${name} marked as global on purpose` : `${name} is no longer marked as global on purpose`),
2303
2366
  moveGlobal: (skills, repos, group) => {
2304
- const moved = [];
2305
- const notes = [];
2306
- const held = [];
2307
2367
  const repoRoots = repos.map(rootOf);
2308
- for (const mv of moveGlobalSkills(skills, repoRoots, { group, label: (root) => repos[repoRoots.indexOf(root)], allowBlind: true })) {
2309
- if (!mv.moved) {
2310
- notes.push(`${mv.name}: ${mv.reason}`);
2311
- continue;
2312
- }
2313
- moved.push(mv.name);
2314
- for (const [i, [root, c]] of mv.changes.entries()) {
2315
- const repo = repos[i];
2316
- const blind = visibilityOf(root, mv.name).flatMap((v) => v.paths === 0 ? [v.id] : []);
2317
- const blocked = [...c.blocked ?? [], ...c.uncommitted ?? []];
2318
- if (!blocked.length || !blind.length) continue;
2319
- held.push({ repo, name: mv.name, blocked, blind, copy: copyDir(repo, mv.name), shared: !c.uncommitted?.length });
2320
- }
2368
+ const label = (root) => repos[repoRoots.indexOf(root)] ?? nameOf(root);
2369
+ const picked = new Set(skills.map((m) => m.name));
2370
+ const sourced = machine.filter((o) => picked.has(o.name) && o.movable && !o.broken);
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" });
2372
+ const said4 = (results2) => {
2373
+ const moved2 = results2.filter((mv) => mv.moved).map((mv) => mv.name);
2374
+ const which = moved2.length <= 4 ? moved2.join(", ") : `${moved2.length} skills`;
2375
+ const summary = moved2.length ? `${which} now load${moved2.length === 1 ? "s" : ""} only in ${repos.join(", ")}` : "";
2376
+ const left = results2.flatMap((mv) => mv.left ?? []);
2377
+ return {
2378
+ moved: moved2,
2379
+ text: [summary, ...left.length ? [`${left.join(", ")} now point${left.length === 1 ? "s" : ""} at nothing: remove ${left.length === 1 ? "it" : "them"}`] : [], ...results2.filter((mv) => !mv.moved).map((mv) => `${mv.name}: ${mv.reason}`)].filter(Boolean).join("; ") || "Nothing moved"
2380
+ };
2381
+ };
2382
+ const results = attempt();
2383
+ const { moved, text } = said4(results);
2384
+ const finishable = (mv) => !mv.moved && mv.changes.length > 0 && mv.changes.every(([, c]) => c.action !== "skipped" && !c.inTheWay?.length && !c.uncommitted?.length) && mv.changes.some(([, c]) => c.blocked?.length);
2385
+ const held = results.filter(finishable).flatMap((mv) => mv.changes.flatMap(([root, c]) => c.blocked?.length ? [{ repo: label(root), name: mv.name, blocked: c.blocked }] : []));
2386
+ if (held.length) {
2387
+ const ask = allowTracked(held);
2388
+ const names = [...new Set(held.map((h) => h.name))];
2389
+ return {
2390
+ message: text,
2391
+ then: {
2392
+ ...ask,
2393
+ label: `${ask.label}, then stop loading ${names.length === 1 ? "it" : "them"} globally`,
2394
+ // The retry is what says whether it's done: a skill still global after it is a failure, said as one.
2395
+ run: (w) => {
2396
+ const linked = said(ask.run(w));
2397
+ const retry = said4(attempt(names));
2398
+ const before = said4(results.filter((mv) => mv.moved));
2399
+ const others = results.filter((mv) => !mv.moved && !names.includes(mv.name)).map((mv) => `${mv.name}: ${mv.reason}`);
2400
+ const text2 = [before.moved.length ? before.text : "", linked, retry.text, ...others].filter(Boolean).join("; ");
2401
+ return retry.moved.length === names.length ? text2 : failed(text2);
2402
+ }
2403
+ }
2404
+ };
2321
2405
  }
2322
- const gaps = /* @__PURE__ */ new Map();
2323
- for (const repo of new Set(held.map((h) => h.repo)))
2324
- for (const shared of [true, false]) {
2325
- const here = held.filter((h) => h.repo === repo && h.shared === shared);
2326
- if (!here.length) continue;
2327
- const agents = [...new Set(here.flatMap((h) => h.blind))].map((id) => harness(id).name).join(", ");
2328
- const what = skills.length === 1 ? "it" : here.length === moved.length ? "them" : here.map((h) => h.name).join(", ");
2329
- const dirs = [...new Set(here.flatMap((h) => h.blocked))].join(", ");
2330
- const why = shared ? `git tracks ${dirs}` : `${[...new Set(here.map((h) => h.copy))].join(", ")} isn't committed, so a link in ${dirs} would break for teammates`;
2331
- const key = `${agents}|${what}|${why}`;
2332
- if (!gaps.has(key)) gaps.set(key, { who: `${agents} can't see ${what}`, why, repos: [] });
2333
- gaps.get(key).repos.push(repo);
2334
- }
2335
- const blindNotes = [...gaps.values()].map((g) => `${g.who} in ${g.repos.join(", ")}: ${g.why}`);
2336
- const summary = moved.length ? `${moved.length === 1 ? moved[0] : `${moved.length} skills`} now load${moved.length === 1 ? "s" : ""} only in ${repos.join(", ")}` : "";
2337
- const text = [summary, ...blindNotes, ...notes].filter(Boolean).join("; ") || "Nothing moved";
2338
- const askable = held.filter((h) => h.shared);
2339
- if (!moved.length && notes.length) return failed(text);
2340
- return askable.length ? { message: text, then: allowTracked(askable) } : text;
2406
+ return moved.length ? text : failed(text);
2341
2407
  },
2342
2408
  createSkill: (name) => {
2343
2409
  const r = createSkill(name, "");
@@ -2346,10 +2412,15 @@ function realOps(roots, backups, machine, libraryNames) {
2346
2412
  replacePlugin: (p, repos) => {
2347
2413
  const id = p.id;
2348
2414
  const clashes = p.skills.filter((s) => {
2349
- const mine = treeHash(librarySkillDir(s.name));
2350
- 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);
2351
2418
  });
2352
- 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`);
2420
+ const cant = repos.flatMap(
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) ?? "")))
2422
+ );
2423
+ if (cant.length) return failed(`${id} kept, nothing changed: ${cant.join("; ")}`);
2353
2424
  const imported = p.skills.map((s) => ({ name: s.name, status: importSkill(s.path).status }));
2354
2425
  for (const r of imported) if (r.status !== "unchanged") recordOrigin(r.name, `plugin: ${id}`);
2355
2426
  const added = repos.length ? addMany(repos, p.skills.map((s) => s.name)) : { result: "", skipped: [], notes: [] };
@@ -2386,21 +2457,31 @@ function realOps(roots, backups, machine, libraryNames) {
2386
2457
  return r.ok ? r.message : failed(r.message);
2387
2458
  },
2388
2459
  deleteLibrary: (name) => {
2460
+ const using = projectsUsing(name);
2461
+ const unreadable = readableRoots(using).unreadable.map((u) => u.root);
2462
+ if (unreadable.length) return failed(`${name} is pinned in ${unreadable.map((r2) => basename(r2)).join(", ")}, whose skilllib.json can't be read: fix it first, then delete`);
2463
+ const hidden = using.filter((root) => ![...roots.values()].includes(root));
2464
+ if (hidden.length) return failed(`${name} is also in ${hidden.map((r2) => basename(r2)).join(", ")}, which ${hidden.length === 1 ? "is" : "are"} hidden: unhide ${hidden.length === 1 ? "it" : "them"} (Settings) to delete it everywhere`);
2389
2465
  const from = [];
2390
- for (const root of projectsUsing(name)) {
2466
+ const left = [];
2467
+ for (const root of using) {
2391
2468
  const s = projectStatus(root).find((x) => x.name === name && x.managed);
2392
- if (s && existsSync3(s.path)) removeUntracked(root, name, s.path);
2393
- removeSkill(root, name, { force: true });
2394
- from.push([...roots].find(([, r2]) => r2 === root)?.[0] ?? basename(root));
2469
+ if (s && isDir(s.path) && !notWritable(root, relative(root, dirname2(s.path)).split(sep).join("/"))) removeUntracked(root, name, s.path);
2470
+ const c = removeSkill(root, name, { force: true });
2471
+ if (c.action === "skipped") return failed(`${name}: ${c.reason}${from.length ? `; removed from ${from.join(", ")} so far` : ""}`);
2472
+ const repo = nameOf(root);
2473
+ from.push(repo);
2474
+ if (c.reason) left.push(`in ${repo}, ${c.reason}`);
2395
2475
  }
2396
2476
  const r = deleteLibrarySkill(name);
2397
2477
  if (!r.ok) return failed(`${name}: ${r.reason}`);
2398
- return `Deleted ${name} from your library${from.length ? ` and from ${from.join(", ")}` : ""} (in Settings \u203A Backups)`;
2478
+ return `Deleted ${name} from your library${from.length ? ` and from ${from.join(", ")}` : ""} (in Settings \u203A Backups)${left.length ? `; ${left.join("; ")}` : ""}`;
2399
2479
  },
2400
2480
  restoreBackup: (i) => {
2401
- const b = backups[i];
2402
- const r = b.kind === "plugin" ? restorePlugin(b) : restoreBackup(b);
2403
- return r.ok ? `Restored ${b.name} to ${tildify2(r.to)}` : failed(`${b.name}: ${r.reason}`);
2481
+ const done = movedTogether(backups, backups[i]).map((b) => [b, b.kind === "plugin" ? restorePlugin(b) : restoreBackup(b)]);
2482
+ const bad = done.filter(([, r]) => !r.ok);
2483
+ if (bad.length) return failed(bad.map(([b, r]) => `${b.name}: ${!r.ok ? r.reason : ""}`).join("; "));
2484
+ return done.map(([b, r]) => `Restored ${b.name} to ${r.ok ? tildify(r.to) : ""}`).join(" \xB7 ");
2404
2485
  },
2405
2486
  setAgents: (ids) => (setHarnesses(ids), "Saved your agents"),
2406
2487
  installVersion: (repo, name, version) => withLinks(addSkill(rootOf(repo), name, { version, force: true }), repo, name),
@@ -2409,33 +2490,34 @@ function realOps(roots, backups, machine, libraryNames) {
2409
2490
  addRoot: (path) => {
2410
2491
  const root = addRoot(path);
2411
2492
  discoverProjects();
2412
- return `Scanning ${tildify2(root)} for repos`;
2493
+ return `Scanning ${tildify(root)} for repos`;
2413
2494
  },
2414
- removeRoot: (path) => (removeRoot(path), `Stopped scanning ${tildify2(path)}; repos already found stay until you hide them`),
2495
+ removeRoot: (path) => (removeRoot(path), `Stopped scanning ${tildify(path)}; repos already found stay until you hide them`),
2415
2496
  rescan: () => (discoverProjects(), "Looked for repos in your project folders again"),
2416
2497
  hide: (repo) => (setHidden(rootOf(repo), true), `Hid ${repo}: Settings \u203A Hidden repos brings it back`),
2417
- unhide: (path) => (setHidden(path, false), `${tildify2(path)} is back in the list`),
2498
+ unhide: (path) => (setHidden(path, false), `${tildify(path)} is back in the list`),
2418
2499
  openFolder: (path) => {
2419
2500
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
2420
- spawn(cmd, [path], { detached: true, stdio: "ignore" }).unref();
2421
- return `Opened ${tildify2(path)}`;
2501
+ const r = spawnSync3(cmd, [path], { stdio: "ignore", timeout: 1e4 });
2502
+ return r.error || r.status !== 0 && process.platform !== "win32" ? failed(`Couldn't open ${tildify(path)} (${cmd} ${r.error ? "isn't available" : "failed"})`) : `Opened ${tildify(path)}`;
2422
2503
  },
2423
2504
  copy: (text, what) => {
2424
2505
  const r = copyText(text);
2425
- return r.copied ? `Copied ${what}: paste it into Claude Code, Cursor, Codex or Grok` : `Couldn't reach the clipboard; ${what} is in ${tildify2(r.savedTo)}`;
2506
+ return r.copied ? `Copied ${what}: paste it into Claude Code, Cursor, Codex or Grok` : `Couldn't reach the clipboard; ${what} is in ${tildify(r.savedTo)}`;
2426
2507
  },
2427
2508
  installAgentSkill: () => {
2428
2509
  const r = installAgentSkill();
2429
- return r.ok ? `Your agents can now use skilllib (${r.dirs.map(tildify2).join(", ")})` : failed(`skilllib skill: ${r.reason}`);
2510
+ return r.ok ? `Your agents can now use skilllib (${r.dirs.map(tildify).join(", ")})` : failed(`skilllib skill: ${r.reason}`);
2430
2511
  },
2431
2512
  removeAgentSkill: () => {
2432
2513
  const removed = removeAgentSkill();
2433
- return removed.length ? `Removed the skilllib skill (${removed.map(tildify2).join(", ")})` : failed("The skilllib skill isn't installed");
2514
+ return removed.length ? `Removed the skilllib skill (${removed.map(tildify).join(", ")})` : failed("The skilllib skill isn't installed");
2434
2515
  },
2435
2516
  usageIssues: ({ usage, totals = {} }) => {
2436
2517
  const repoOf = new Map([...roots].map(([name, root]) => [root, name]));
2437
2518
  return usageIssues([...roots.values()], machine, libraryNames, {
2438
- 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),
2439
2521
  total: (skill) => totals[skill] ?? 0,
2440
2522
  asUsed: (key) => totals[key] ?? 0,
2441
2523
  days: 30
@@ -2447,6 +2529,7 @@ function realOps(roots, backups, machine, libraryNames) {
2447
2529
  // src/tui/index.tsx
2448
2530
  import { jsx as jsx3 } from "react/jsx-runtime";
2449
2531
  async function tui() {
2532
+ discoverProjects();
2450
2533
  const instance = render(/* @__PURE__ */ jsx3(App, { initial: loadWorld(), reload: loadWorld, loadUsage }), { alternateScreen: true, exitOnCtrlC: true });
2451
2534
  await instance.waitUntilExit();
2452
2535
  }