skilllib 1.11.3 → 1.11.4

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,13 @@ import {
20
24
  harness,
21
25
  harnessesChosen,
22
26
  importSkill,
27
+ inTheWaySaid,
23
28
  installAgentSkill,
29
+ installDirs,
24
30
  installedPlugins,
31
+ isDir,
25
32
  keptGlobal,
33
+ knownProjects,
26
34
  latestVersion,
27
35
  libraryOrigins,
28
36
  librarySkillDir,
@@ -32,6 +40,8 @@ import {
32
40
  localStamp,
33
41
  machineSkills,
34
42
  moveGlobalSkills,
43
+ movedTogether,
44
+ notWritable,
35
45
  pluginBackups,
36
46
  projectHere,
37
47
  projectOfFactory,
@@ -41,9 +51,11 @@ import {
41
51
  readManifest,
42
52
  readSkillInfo,
43
53
  readableRoots,
54
+ realPath,
44
55
  recordOrigin,
45
56
  relativeTo,
46
57
  relinkDependency,
58
+ rememberProjects,
47
59
  removeAgentSkill,
48
60
  removeRoot,
49
61
  removeSkill,
@@ -56,18 +68,21 @@ import {
56
68
  setHidden,
57
69
  setKeepGlobal,
58
70
  setPluginOn,
71
+ skillOf,
59
72
  skilllibHome,
60
73
  skillsLoadedIn,
74
+ strandedLinks,
75
+ survives,
61
76
  tildify,
77
+ trackBlocker,
62
78
  treeHash,
63
79
  uninstallPlugin,
64
80
  updatePlugin,
65
81
  updateProject,
66
82
  usageIssues,
67
83
  versionHistory,
68
- visibilityOf,
69
84
  visibleProjects
70
- } from "./chunk-3XDWEHI4.js";
85
+ } from "./chunk-CS6T66EE.js";
71
86
 
72
87
  // src/tui/index.tsx
73
88
  import { render } from "ink";
@@ -186,6 +201,9 @@ function wrap(text, width, max) {
186
201
  return lines;
187
202
  }
188
203
 
204
+ // src/tui/prompts.ts
205
+ import { dirname } from "path";
206
+
189
207
  // src/review.ts
190
208
  import { spawnSync } from "child_process";
191
209
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
@@ -194,7 +212,7 @@ var EXCERPT_CHARS = 1200;
194
212
  function skillExcerpt(dir, chars) {
195
213
  const file = join(dir, "SKILL.md");
196
214
  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?/, "");
215
+ const content = readFileSync(file, "utf-8").replace(/^\uFEFF/, "").replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "");
198
216
  let files = 0;
199
217
  try {
200
218
  files = readdirSync(dir, { recursive: true, withFileTypes: true }).filter((d) => d.isFile()).length;
@@ -296,8 +314,9 @@ var isFailure = (r) => typeof r !== "string" && "failed" in r;
296
314
  var followUp = (r) => typeof r !== "string" && "then" in r ? r.then : void 0;
297
315
  var said = (r) => typeof r === "string" ? r : r.message;
298
316
  var joined = (results) => {
299
- const message = results.map(said).join(" \xB7 ");
300
- return results.some(isFailure) ? failed(message) : message;
317
+ const bad = results.filter(isFailure);
318
+ const message = [...bad.length && bad.length < results.length ? [`${results.length - bad.length} of ${results.length} done`] : [], ...results.map(said)].join(" \xB7 ");
319
+ return bad.length ? failed(message) : message;
301
320
  };
302
321
  var SEVERITY_RANK = { problem: 0, suggestion: 1 };
303
322
  function fixOf(c, found) {
@@ -325,6 +344,7 @@ function shortOf(found) {
325
344
  sync: "Folder missing",
326
345
  unavailable: "Pinned, not in your library",
327
346
  pin: "Pinned version not in your library",
347
+ "out-of-sync": "Not the pinned version",
328
348
  update: "Out of date",
329
349
  uncommitted: "Commit it first",
330
350
  local: "Not in your library",
@@ -353,7 +373,7 @@ function viewOf(found) {
353
373
  title: found.title,
354
374
  short: shortOf(found),
355
375
  detail: found.detail,
356
- skills: [...new Set(found.at?.map((a) => a.skill))],
376
+ skills: [...new Set(found.at?.flatMap((a) => a.skill ? [a.skill] : []))],
357
377
  decision: !found.fix,
358
378
  fixes: (found.fix ? [found.fix] : found.choices ?? []).map((c) => fixOf(c, found))
359
379
  };
@@ -364,6 +384,9 @@ function filterOf(source) {
364
384
  function project(w, name) {
365
385
  return w.projects.find((p) => p.name === name);
366
386
  }
387
+ function usesKey(m) {
388
+ return m.source === "plugin" ? `${m.where.split("@")[0]}:${m.name}` : m.name;
389
+ }
367
390
  function totalUses(w, name) {
368
391
  return Object.values(w.usage).reduce((n, u) => n + (u[name] ?? 0), 0);
369
392
  }
@@ -383,7 +406,7 @@ function usable(w, projectName) {
383
406
  const uses = (n) => w.usage[projectName]?.[n] ?? 0;
384
407
  return [
385
408
  ...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 }))
409
+ ...machineIn(w, p).map((m) => ({ name: m.name, source: m.source, where: m.where, agents: m.agents, uses: uses(usesKey(m)), machine: m }))
387
410
  ];
388
411
  }
389
412
  function issuesAt(w, repo, skill) {
@@ -397,7 +420,7 @@ function repoIssues(w, repo) {
397
420
  function unlisted(w, repo) {
398
421
  const root = project(w, repo).path;
399
422
  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)))];
423
+ 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
424
  }
402
425
  function libraryIssues(w, name) {
403
426
  return w.issues.filter((i) => i.id === "unused:library" && i.at?.some((a) => a.skill === name)).map(issueOf);
@@ -410,12 +433,12 @@ function globalFixes(m) {
410
433
  return [
411
434
  {
412
435
  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).`,
436
+ preview: `Pick repos. ${m.name} goes into your library and those repos, and stops loading globally (every global copy backed up).`,
414
437
  run: () => "",
415
438
  pickRepos: (w, repos) => w.ops.moveGlobal([m], repos)
416
439
  },
417
440
  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) }
441
+ { label: "Delete it", preview: `Remove ${m.where}/${m.name}. A backup is kept in Settings \u203A Backups.`, run: (w) => w.ops.deleteGlobal(m) }
419
442
  ];
420
443
  }
421
444
  function machineActions(m) {
@@ -466,7 +489,7 @@ function deleteLibraryFix(w, names) {
466
489
  `Delete ${what} and ${names.length === 1 ? "its" : "their"} versions from your library.`,
467
490
  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
491
  edited.length ? `${edited.join(", ")} ${edited.length === 1 ? "has" : "have"} local edits: backed up too.` : "",
469
- "Every copy goes to Settings \u203A Backups.",
492
+ "Every copy goes to Settings \u203A Backups; where a repo commits its copy, git will see it.",
470
493
  others.length ? `Untracked or the team's own copies in ${others.join(", ")} stay.` : ""
471
494
  ].filter(Boolean).join(" "),
472
495
  run: (w2) => joined(names.map((n) => w2.ops.deleteLibrary(n)))
@@ -495,7 +518,8 @@ function replacePluginFix(p) {
495
518
  `Copies its ${plural(names.length, "skill")} into your library and adds them to the repos you tick (or none),`,
496
519
  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
520
  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."
521
+ // A plugin only turned on in settings isn't listed in Plugins once it's off: say where it comes back.
522
+ offOnly ? p.settingsOnly ? "Turn it back on with /plugin in Claude Code." : "Turn it back on from Plugins." : "Settings \u203A Backups reinstalls it."
499
523
  ].filter(Boolean).join(" "),
500
524
  run: () => "",
501
525
  allowNone: true,
@@ -638,14 +662,16 @@ var agentNames = (agents) => agents.map((a) => harness(a).name).join(", ") || "n
638
662
  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
663
  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
664
  function reviewPrompt(w, names, scope, repo) {
665
+ const project2 = repo ? w.projects.find((p) => p.name === repo) : void 0;
666
+ const machine = project2 ? machineIn(w, project2) : w.machine;
641
667
  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;
668
+ const copies = machine.filter((m2) => m2.name === name);
669
+ const here = project2?.skills.find((s) => s.name === name);
644
670
  if (here) {
645
671
  const lib2 = w.library.find((l) => l.name === name);
646
672
  return {
647
673
  name,
648
- description: w.descriptions[name] ?? "",
674
+ description: here.description ?? w.descriptions[name] ?? "",
649
675
  source: localSource(here),
650
676
  installedHow: localInstalled(here),
651
677
  path: here.path,
@@ -669,12 +695,13 @@ function reviewPrompt(w, names, scope, repo) {
669
695
  description: w.descriptions[name] ?? "",
670
696
  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
697
  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 ?? ""),
698
+ path: m?.path ?? (lib ? dirname(w.ops.libraryFile(name)) : local?.path ?? ""),
673
699
  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",
700
+ 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
701
  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]]] : []),
702
+ // A plugin's copy is counted under plugin:skill.
703
+ usesTotal: w.days ? totalUses(w, m ? usesKey(m) : name) : null,
704
+ usesByProject: Object.entries(w.usage).flatMap(([p, u]) => /* @__PURE__ */ ((n) => n ? [[p, n]] : [])(u[m ? usesKey(m) : name])),
678
705
  installedIn: repos.map((p) => p.name),
679
706
  otherCopies: copies.filter((c) => c !== m).map((c) => `${c.source} ${c.where}`),
680
707
  inYourSkills: lib ? `yes, v${lib.latest}` : null,
@@ -690,7 +717,7 @@ function reviewPrompt(w, names, scope, repo) {
690
717
  }
691
718
  function writeSkillPrompt(w, name, repo) {
692
719
  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>");
720
+ const file = p ? `${p.path}/${installDirs(w.agents)[0]}/${name || "<name>"}/SKILL.md` : w.ops.libraryFile(name || "<name>");
694
721
  return `Write a new agent skill${name ? ` named "${name}"` : ""}${p ? ` for the repo at ${p.path}` : " for my skill library"}.
695
722
 
696
723
  Create ${file} with:
@@ -739,7 +766,7 @@ function tag(u) {
739
766
  return [u.source, color.muted];
740
767
  }
741
768
  function isNameChar(input, key) {
742
- return !key.ctrl && !key.meta && /^[a-zA-Z0-9._-]$/.test(input);
769
+ return !key.ctrl && !key.meta && /^[a-zA-Z0-9._-]+$/.test(input);
743
770
  }
744
771
  function clip(text, width) {
745
772
  return text.length > width - 1 ? text.slice(0, width - 2) + "\u2026" : text;
@@ -755,7 +782,7 @@ function builder(title) {
755
782
  issues(issues) {
756
783
  if (!issues.length) return d.body.push({ segs: [["\u2713 No issues", color.green]] });
757
784
  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]] });
785
+ 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
786
  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
787
  }
761
788
  return 0;
@@ -848,13 +875,14 @@ function skillItem(w, copies, issues, nameW, actions, repo) {
848
875
  ],
849
876
  detail: (width) => {
850
877
  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]);
878
+ for (const l of wrap(u.local?.description || w.descriptions[u.name] || "(no description)", width - 4, 3)) b.line([l, color.muted]);
852
879
  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]);
880
+ const here = !!u.machine && !w.machine.some((m) => m.path === u.machine.path && m.where === u.machine.where);
881
+ 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
882
  else b.issues(issues);
855
883
  b.actions(actions, width);
856
884
  b.line();
857
- for (const segs of usageLines(w, u.name, width, repo)) b.line(...segs);
885
+ for (const segs of usageLines(w, u.machine ? usesKey(u.machine) : u.name, width, repo)) b.line(...segs);
858
886
  b.line();
859
887
  b.line(["Loaded from", color.accent]);
860
888
  for (const c of copies) {
@@ -904,12 +932,13 @@ var reviewOption = (w, name, ui, repo) => ({
904
932
  label: "Review prompt",
905
933
  action: () => ui.copy(reviewPrompt(w, [name], `Review the skill ${name}${repo ? ` in ${repo}` : ""}.`, repo), `a review prompt for ${name}`)
906
934
  });
935
+ var gitSees = (s) => s?.git === "committed" || s?.git === "changed" ? " Git will see it." : "";
907
936
  function versionsOption(w, repo, name, installed, ui) {
908
937
  return {
909
938
  label: "Other versions\u2026",
910
939
  action: () => ui.menu(
911
940
  `${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) }))
941
+ 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
942
  )
914
943
  };
915
944
  }
@@ -919,14 +948,15 @@ function skillActions(w, p, u, ui) {
919
948
  const remove = {
920
949
  // Its folder gone: all that's left to remove is the skilllib.json entry (the label Health offers too).
921
950
  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.` : ""),
951
+ 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.
952
+ (gitSees(s) || (s?.source === "lib" && project(w, p).manifest === "committed" ? " Git will see skilllib.json change." : "")),
923
953
  run: (w2) => w2.ops.remove(p, u.name)
924
954
  };
925
955
  if (s?.source === "lib")
926
956
  return [
927
957
  // A tracked skill your library doesn't have: nothing to edit there, no versions to pick.
928
958
  ...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) })] : [],
959
+ ...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
960
  { label: "Edit in library", action: () => ui.edit(w.ops.libraryFile(u.name)) },
931
961
  versionsOption(w, p, u.name, s.version, ui)
932
962
  ] : [],
@@ -945,7 +975,8 @@ function skillActions(w, p, u, ui) {
945
975
  fixOption(remove),
946
976
  reviewOption(w, u.name, ui, p)
947
977
  ];
948
- return [{ label: "Open in Global", action: () => ui.openInGlobal(u.name) }, reviewOption(w, u.name, ui)];
978
+ const everywhere = !!u.machine && w.machine.some((m) => m.path === u.machine.path && m.where === u.machine.where);
979
+ return everywhere ? [{ label: "Open in Global", action: () => ui.openInGlobal(u.name) }, reviewOption(w, u.name, ui)] : [reviewOption(w, u.name, ui, p)];
949
980
  }
950
981
  function overview(w, p) {
951
982
  const project2 = project(w, p);
@@ -992,7 +1023,7 @@ function projectItems(w) {
992
1023
  });
993
1024
  }
994
1025
  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 }));
1026
+ 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
1027
  return byName(copies).map((g) => skillItem(w, g, issuesAt(w, null, g[0].name), nameW, globalActions(w, g[0], ui), null));
997
1028
  }
998
1029
  var AT_SOURCE = {
@@ -1055,7 +1086,7 @@ function libraryItems(w, nameW, ui) {
1055
1086
  ...removable.length ? [
1056
1087
  fixOption({
1057
1088
  label: "Remove from repos\u2026",
1058
- preview: `Pick repos to remove ${l.name} from. Your library keeps it.`,
1089
+ preview: `Pick repos to remove ${l.name} from. Your library keeps it. Where a repo commits its copy, git will see it.`,
1059
1090
  run: () => "",
1060
1091
  candidates: () => removable.map((p) => p.name),
1061
1092
  pickRepos: (w2, rs) => joined(rs.map((r) => w2.ops.remove(r, l.name)))
@@ -1149,7 +1180,7 @@ function pluginItems(w, columns, skillW, ui, issuesOf) {
1149
1180
  const skills = p.skills.map((s) => cursor || !w.days ? s.name : `${s.name} (${uses2(totalUses(w, pluginSkillKey(p, s.name)))})`);
1150
1181
  wrap(skills.join(" \xB7 ") || "none", width - 16, 4).forEach((l, i) => b.line(label(i ? "" : "Skills"), [l, color.text]));
1151
1182
  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`;
1183
+ 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
1184
  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
1185
  b.line(label("Folder"), [tildify(p.path), color.faint]);
1155
1186
  return b.d;
@@ -1174,7 +1205,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1174
1205
  const world = { ...withUsage, issues: [...base.issues, ...usageIssues2] };
1175
1206
  useEffect(() => {
1176
1207
  let live = true;
1177
- void loadUsage2?.(base).then((u) => live && setUsed(u));
1208
+ void loadUsage2?.(base).then((u) => live && setUsed(u)).catch(() => live && setToast("\u2717 Couldn't read your Claude Code transcripts: usage isn't shown"));
1178
1209
  return () => {
1179
1210
  live = false;
1180
1211
  };
@@ -1253,7 +1284,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1253
1284
  }
1254
1285
  function choose(o) {
1255
1286
  if (o.action) return o.action();
1256
- if (!o.fix) return setToast("Not in the prototype");
1287
+ if (!o.fix) return;
1257
1288
  if (o.fix.pickRepos) {
1258
1289
  const name = current?.name ?? "";
1259
1290
  const only = o.fix.candidates?.(world);
@@ -1319,7 +1350,8 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1319
1350
  cells: [
1320
1351
  { text: top ? SEV[top.severity].icon : " ", width: 2, color: top ? SEV[top.severity].color : void 0 },
1321
1352
  { 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 }
1353
+ // A leading space keeps it apart from a name cut to fit.
1354
+ { text: flagged ? ` ${flagged} to fix` : "", width: 11, color: top ? SEV[top.severity].color : color.faint }
1323
1355
  ],
1324
1356
  detail: (width) => {
1325
1357
  const b = builder(label);
@@ -1398,7 +1430,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1398
1430
  () => ui.menu(repo, [
1399
1431
  { label: "Open its folder", action: () => setToast(said2(world.ops.openFolder(project(world, repo).path))) },
1400
1432
  { 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}`) }] : []
1433
+ ...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
1434
  ])
1403
1435
  )
1404
1436
  ];
@@ -1446,8 +1478,16 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1446
1478
  setToast(`Saved ${tildify(file)}`);
1447
1479
  });
1448
1480
  }
1449
- function askForFolder(title) {
1450
- setModal({ kind: "input", title, value: "", placeholder: "~/Projects", submit: (v) => apply((w) => w.ops.addRoot(v)) });
1481
+ function askForFolder(title, problem) {
1482
+ const submit = (v) => apply((w) => {
1483
+ try {
1484
+ return w.ops.addRoot(v);
1485
+ } catch (e) {
1486
+ askForFolder(title, e.message);
1487
+ throw e;
1488
+ }
1489
+ });
1490
+ setModal({ kind: "input", title: problem ? `${problem}. ${title}` : title, value: "", placeholder: "~/Projects", submit });
1451
1491
  }
1452
1492
  useEffect(() => {
1453
1493
  const folder = () => !initial.roots.length && askForFolder("Where are your projects? skilllib finds the git repos in there");
@@ -1701,7 +1741,8 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1701
1741
  if (isNameChar(input, key)) return setModal({ ...m, query: m.query + input.toLowerCase(), cursor: 0 });
1702
1742
  }
1703
1743
  }
1704
- const bottomH = 1;
1744
+ const toastLines = toast && modal?.kind !== "confirm" ? wrap(toast, termCols - 2, 3).map((l) => " " + l) : [];
1745
+ const bottomH = Math.max(1, toastLines.length);
1705
1746
  const confirmLines = modal?.kind === "confirm" ? wrap(modal.fix.preview, columns - 6, 6) : [];
1706
1747
  const confirmH = modal?.kind === "confirm" ? confirmLines.length + 2 : 0;
1707
1748
  const bodyH = Math.max(8, termRows - (sidebar ? 0 : 1) - bottomH - confirmH);
@@ -1952,9 +1993,9 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1952
1993
  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
1994
  ] })
1954
1995
  ] }),
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 + " " })
1996
+ /* @__PURE__ */ jsxs2(Box2, { height: bottomH, width: termCols, justifyContent: "space-between", children: [
1997
+ 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 }),
1998
+ toast && modal?.kind !== "confirm" ? null : /* @__PURE__ */ jsx2(Text2, { color: color.faint, children: " " + hints + " " })
1958
1999
  ] })
1959
2000
  ] });
1960
2001
  }
@@ -1999,7 +2040,7 @@ function Help({ width, height }) {
1999
2040
  const notes = [
2000
2041
  '\u2715 problem \xB7 \u26A0 suggestion \xB7 \u2605 recommended fix \xB7 "your call" = a decision, never auto-fixed',
2001
2042
  "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."
2043
+ "Usage counts Claude Code sessions only: skilllib reads no other agent's transcripts."
2003
2044
  ];
2004
2045
  const lines = [
2005
2046
  ...keys.flatMap(([k, d]) => wrap(d, width - 16, 3).map((l, i) => [[(i ? "" : k).padEnd(12), color.accent], [l, color.text]])),
@@ -2010,12 +2051,9 @@ function Help({ width, height }) {
2010
2051
  }
2011
2052
 
2012
2053
  // 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
- }
2054
+ import { spawnSync as spawnSync3 } from "child_process";
2055
+ import { existsSync as existsSync3, lstatSync } from "fs";
2056
+ import { basename, dirname as dirname2, join as join2, relative, sep } from "path";
2019
2057
  function uniqueNames(roots) {
2020
2058
  const tail = (r, n) => r.split(/[\\/]/).filter(Boolean).slice(-n).join("/");
2021
2059
  return new Map(
@@ -2059,7 +2097,8 @@ function localSkill(root, s, git) {
2059
2097
  };
2060
2098
  }
2061
2099
  function otherCopies(root, name, dir) {
2062
- return foldersOf(root, name).real.map((d) => relativeTo(root, d)).filter((d) => d !== dir);
2100
+ const self = realPath(join2(root, dir, name));
2101
+ return foldersOf(root, name).real.filter((d) => realPath(join2(d, name)) !== self).map((d) => relativeTo(root, d));
2063
2102
  }
2064
2103
  function installedAt(path) {
2065
2104
  try {
@@ -2073,8 +2112,8 @@ function machineSkill(s, kept) {
2073
2112
  const global = s.kind === "global" || s.kind === "skills.sh";
2074
2113
  return {
2075
2114
  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,
2115
+ 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",
2116
+ 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
2117
  ...s.kind === "skills.sh" ? { origin: `npx skills: ${s.origin}` } : {},
2079
2118
  path: s.path,
2080
2119
  links: s.links,
@@ -2111,13 +2150,8 @@ function loadWorld() {
2111
2150
  forgetClaudeBinary();
2112
2151
  const agents = enabledHarnesses();
2113
2152
  const here = projectHere();
2114
- const real = (p) => {
2115
- try {
2116
- return realpathSync(p);
2117
- } catch {
2118
- return p;
2119
- }
2120
- };
2153
+ if (here) rememberProjects([here]);
2154
+ const real = realPath;
2121
2155
  const visible = visibleProjects();
2122
2156
  const cwd = here ? visible.find((p) => real(p) === real(here)) ?? here : null;
2123
2157
  const listed = [.../* @__PURE__ */ new Set([...cwd ? [cwd] : [], ...visible])];
@@ -2132,7 +2166,7 @@ function loadWorld() {
2132
2166
  const libraryNames = new Set(library.map((l) => l.name));
2133
2167
  const kept = keptGlobal();
2134
2168
  const cursorList = agents.includes("cursor") ? cursorPlugins() : [];
2135
- const sourced = machineSkills(agents);
2169
+ const sourced = machineSkills(agents).filter((s) => s.harnesses.length || s.broken);
2136
2170
  const machine = sourced.map((s) => {
2137
2171
  descriptions[s.name] ??= s.description;
2138
2172
  return machineSkill(s, kept);
@@ -2153,7 +2187,11 @@ function loadWorld() {
2153
2187
  name: names.get(root),
2154
2188
  path: root,
2155
2189
  manifest: hasManifest ? git ? git.of("skilllib.json") : "no git" : "none",
2156
- skills: status.map((s) => localSkill(root, s, git)),
2190
+ skills: status.map((s) => {
2191
+ const local = localSkill(root, s, git);
2192
+ const own = s.state.includes("missing") ? "" : readSkillInfo(s.path).description;
2193
+ return own && own !== descriptions[s.name] ? { ...local, description: own } : local;
2194
+ }),
2157
2195
  ...loadedHere(root)
2158
2196
  };
2159
2197
  });
@@ -2169,7 +2207,7 @@ function loadWorld() {
2169
2207
  library,
2170
2208
  usage: {},
2171
2209
  descriptions,
2172
- backups: backups.map((b) => ({ name: b.name, from: tildify2(b.from), at: b.movedAt })),
2210
+ backups: backups.map((b) => ({ name: b.name, from: tildify(b.from), at: b.movedAt })),
2173
2211
  ...(({ roots: roots2, hidden }) => ({ roots: roots2, hidden }))(readConfig()),
2174
2212
  agentsChosen: harnessesChosen(),
2175
2213
  agentSkill: agentSkillState(agents),
@@ -2186,7 +2224,7 @@ async function loadUsage(w) {
2186
2224
  const today = midnight(Date.now());
2187
2225
  const totals = {};
2188
2226
  for (const use of await scanUsage(30)) {
2189
- const skill = use.skill.split(":").pop();
2227
+ const skill = skillOf(use.skill);
2190
2228
  totals[skill] = (totals[skill] ?? 0) + 1;
2191
2229
  if (skill !== use.skill) totals[use.skill] = (totals[use.skill] ?? 0) + 1;
2192
2230
  const root = projectOf(use.cwd);
@@ -2200,7 +2238,9 @@ async function loadUsage(w) {
2200
2238
  }
2201
2239
  function said3(c, repo) {
2202
2240
  if (c.action === "skipped") return `${c.name}: ${c.reason ?? "skipped"}`;
2203
- if (c.action === "removed") return `Removed ${c.name} from ${repo}`;
2241
+ if (c.action === "removed") return `Removed ${c.name} from ${repo}${c.reason ? ` (${c.reason})` : ""}`;
2242
+ if (c.action === "unchanged") return `${c.name}${c.to ? ` v${c.to}` : ""} is already in ${repo}`;
2243
+ if (c.action === "relinked") return `Linked ${c.name}${c.to ? ` v${c.to}` : ""} for your other agents in ${repo}`;
2204
2244
  const back = c.action === "updated" && c.from !== void 0 && c.to !== void 0 && c.to < c.from;
2205
2245
  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
2246
  return c.backedUp ? `${done} (your edits are in Settings \u203A Backups)` : done;
@@ -2212,10 +2252,11 @@ function realOps(roots, backups, machine, libraryNames) {
2212
2252
  const copyDir = (repo, name) => readManifest(rootOf(repo)).skills[name]?.dir ?? PROJECT_SKILLS_DIR;
2213
2253
  const withLinks = (c, repo, name) => {
2214
2254
  const it = (dirs) => dirs.length === 1 ? "it" : "them";
2255
+ const way = c.inTheWay?.length ? `; not linked everywhere: ${inTheWaySaid(rootOf(repo), c.inTheWay)} (clear the way, then link again)` : "";
2215
2256
  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 }]) };
2257
+ 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`;
2258
+ if (!c.blocked?.length) return way ? failed(`${said3(c, repo)}${way}`) : result(c, repo);
2259
+ 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
2260
  };
2220
2261
  const addTo = (repos, names) => {
2221
2262
  if (repos.length === 1 && names.length === 1) return withLinks(addSkill(rootOf(repos[0]), names[0]), repos[0], names[0]);
@@ -2237,10 +2278,11 @@ function realOps(roots, backups, machine, libraryNames) {
2237
2278
  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
2279
  notes.push(`${who}not linked in ${dirs.join(", ")} in ${repo}: ${why}`);
2239
2280
  }
2281
+ 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
2282
  const skipped = changes.filter((x) => x.c.action === "skipped").map((x) => `${x.name} not added to ${x.repo}: ${x.c.reason}`);
2241
2283
  const text = [summary, ...notes, ...skipped].filter(Boolean).join("; ");
2242
2284
  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;
2285
+ 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
2286
  return { result: result2, skipped, notes };
2245
2287
  };
2246
2288
  const allowTracked = (held) => {
@@ -2255,7 +2297,7 @@ function realOps(roots, backups, machine, libraryNames) {
2255
2297
  const linked = results.filter((x) => x.r.created.length);
2256
2298
  const names = [...new Set(linked.map((x) => x.name))];
2257
2299
  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)}`);
2300
+ 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
2301
  return [summary, ...others].join("; ");
2260
2302
  }
2261
2303
  };
@@ -2264,15 +2306,19 @@ function realOps(roots, backups, machine, libraryNames) {
2264
2306
  const s = find(repo, name);
2265
2307
  return s.managed ? relinkDependency(rootOf(repo), name, { allowTracked: allow }) : linkEverywhere(rootOf(repo), name, s.location, { allowTracked: allow });
2266
2308
  };
2267
- const linkSaid = (name, r) => {
2309
+ const linkSaid = (repo, name, r) => {
2268
2310
  const linked = r.created.length ? `linked in ${r.created.join(", ")}` : "";
2269
2311
  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"}`;
2312
+ const inTheWay = r.inTheWay.length ? inTheWaySaid(rootOf(repo), r.inTheWay) : "";
2313
+ return `${name}: ${[linked, notLinked, inTheWay].filter(Boolean).join("; ") || "nothing to link"}`;
2271
2314
  };
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);
2315
+ const orForce = (c, repo, label, force) => (
2316
+ // Whatever --force would get past (edits, a pin from another library, another copy in the way): offer it here.
2317
+ c.action === "skipped" && !!c.reason?.includes("--force") ? {
2318
+ message: `${c.name} in ${repo}: ${c.reason.replace(/ \(use --force[^)]*\)$/, "")}`,
2319
+ then: { label, preview: `What's there now in ${repo} goes to Settings \u203A Backups first.`, run: () => result(force(), repo) }
2320
+ } : result(c, repo)
2321
+ );
2276
2322
  const toLibrary = (path) => {
2277
2323
  const r = importSkill(path, { force: true });
2278
2324
  return r.status === "unchanged" ? `${r.name} is already the same in your library` : `${r.name} v${latestVersion(r.name)?.version} is in your library`;
@@ -2296,48 +2342,64 @@ function realOps(roots, backups, machine, libraryNames) {
2296
2342
  },
2297
2343
  copyToLibrary: (repo, name) => toLibrary(find(repo, name).path),
2298
2344
  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}`);
2345
+ if (!entryExists(m.path)) return `${tildify(m.path)} is already gone`;
2346
+ const others = machine.filter((o) => o.name === m.name && o.path !== m.path && o.kind !== "plugin" && !o.broken);
2347
+ const dependents = others.filter((o) => !survives(o.path, [m.path, ...m.links]));
2348
+ const stays = others.filter((o) => !dependents.includes(o));
2349
+ if (!m.broken) {
2350
+ const readers = [.../* @__PURE__ */ new Set([...m.agents, ...dependents.flatMap((o) => o.harnesses)])].filter((id) => !stays.some((o) => o.harnesses.includes(id)));
2351
+ const blind = blindWithoutGlobal(m.name, [{ path: m.path, harnesses: readers, links: [...m.links, ...dependents.map((o) => o.path)] }], [...roots.values(), ...knownProjects()]);
2352
+ if (blind.length) return failed(`${m.name} kept: ${blindSaid(m.name, blind, (root) => [...roots].find(([, r2]) => r2 === root)?.[0] ?? basename(root))}`);
2353
+ }
2354
+ const left = m.broken ? [] : strandedLinks(m.name, [{ path: m.path, links: [...m.links, ...dependents.map((o) => o.path)] }], [...roots.values(), ...knownProjects()], (root) => [...roots].find(([, r2]) => r2 === root)?.[0] ?? basename(root));
2355
+ const r = deleteGlobal(m.path, [...m.links, ...dependents.map((o) => o.path)]);
2356
+ if (!r.ok) return failed(`${m.name}: ${r.reason}`);
2357
+ if (m.broken) return `Removed the broken link ${m.name}`;
2358
+ const still = stays.map((o) => tildify(o.path));
2359
+ 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
2360
  },
2302
2361
  keepGlobal: (name, keep) => (setKeepGlobal([name], keep), keep ? `${name} marked as global on purpose` : `${name} is no longer marked as global on purpose`),
2303
2362
  moveGlobal: (skills, repos, group) => {
2304
- const moved = [];
2305
- const notes = [];
2306
- const held = [];
2307
2363
  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
- }
2364
+ const label = (root) => repos[repoRoots.indexOf(root)] ?? [...roots].find(([, r]) => r === root)?.[0] ?? basename(root);
2365
+ const picked = new Set(skills.map((m) => m.name));
2366
+ const sourced = machine.filter((o) => picked.has(o.name) && o.movable && !o.broken);
2367
+ const attempt = (names) => moveGlobalSkills(names ? sourced.filter((m) => names.includes(m.name)) : sourced, repoRoots, { group, label, allowTrackedHint: "allow links there, then move it again" });
2368
+ const said4 = (results2) => {
2369
+ const moved2 = results2.filter((mv) => mv.moved).map((mv) => mv.name);
2370
+ const which = moved2.length <= 4 ? moved2.join(", ") : `${moved2.length} skills`;
2371
+ const summary = moved2.length ? `${which} now load${moved2.length === 1 ? "s" : ""} only in ${repos.join(", ")}` : "";
2372
+ const left = results2.flatMap((mv) => mv.left ?? []);
2373
+ return {
2374
+ moved: moved2,
2375
+ 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"
2376
+ };
2377
+ };
2378
+ const results = attempt();
2379
+ const { moved, text } = said4(results);
2380
+ 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);
2381
+ const held = results.filter(finishable).flatMap((mv) => mv.changes.flatMap(([root, c]) => c.blocked?.length ? [{ repo: label(root), name: mv.name, blocked: c.blocked }] : []));
2382
+ if (held.length) {
2383
+ const ask = allowTracked(held);
2384
+ const names = [...new Set(held.map((h) => h.name))];
2385
+ return {
2386
+ message: text,
2387
+ then: {
2388
+ ...ask,
2389
+ label: `${ask.label}, then stop loading ${names.length === 1 ? "it" : "them"} globally`,
2390
+ // The retry is what says whether it's done: a skill still global after it is a failure, said as one.
2391
+ run: (w) => {
2392
+ const linked = said(ask.run(w));
2393
+ const retry = said4(attempt(names));
2394
+ const before = said4(results.filter((mv) => mv.moved));
2395
+ const others = results.filter((mv) => !mv.moved && !names.includes(mv.name)).map((mv) => `${mv.name}: ${mv.reason}`);
2396
+ const text2 = [before.moved.length ? before.text : "", linked, retry.text, ...others].filter(Boolean).join("; ");
2397
+ return retry.moved.length === names.length ? text2 : failed(text2);
2398
+ }
2399
+ }
2400
+ };
2321
2401
  }
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;
2402
+ return moved.length ? text : failed(text);
2341
2403
  },
2342
2404
  createSkill: (name) => {
2343
2405
  const r = createSkill(name, "");
@@ -2350,6 +2412,10 @@ function realOps(roots, backups, machine, libraryNames) {
2350
2412
  return mine !== null && mine !== treeHash(s.path);
2351
2413
  });
2352
2414
  if (clashes.length) return failed(`${id} kept: your library has different ${clashes.map((s) => s.name).join(", ")}; update or delete ${clashes.length === 1 ? "it" : "them"} first`);
2415
+ const cant = repos.flatMap(
2416
+ (repo) => p.skills.flatMap((s) => ((why) => why ? [`${s.name} in ${repo}: ${why.replace(/ \(use --force[^)]*\)$/, "")}`] : [])(trackBlocker(rootOf(repo), s.name) ?? addRefusal(rootOf(repo), s.name, treeHash(s.path) ?? "")))
2417
+ );
2418
+ if (cant.length) return failed(`${id} kept, nothing changed: ${cant.join("; ")}`);
2353
2419
  const imported = p.skills.map((s) => ({ name: s.name, status: importSkill(s.path).status }));
2354
2420
  for (const r of imported) if (r.status !== "unchanged") recordOrigin(r.name, `plugin: ${id}`);
2355
2421
  const added = repos.length ? addMany(repos, p.skills.map((s) => s.name)) : { result: "", skipped: [], notes: [] };
@@ -2386,21 +2452,31 @@ function realOps(roots, backups, machine, libraryNames) {
2386
2452
  return r.ok ? r.message : failed(r.message);
2387
2453
  },
2388
2454
  deleteLibrary: (name) => {
2455
+ const using = projectsUsing(name);
2456
+ const unreadable = readableRoots(using).unreadable.map((u) => u.root);
2457
+ 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`);
2458
+ const hidden = using.filter((root) => ![...roots.values()].includes(root));
2459
+ 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
2460
  const from = [];
2390
- for (const root of projectsUsing(name)) {
2461
+ const left = [];
2462
+ for (const root of using) {
2391
2463
  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));
2464
+ if (s && isDir(s.path) && !notWritable(root, relative(root, dirname2(s.path)).split(sep).join("/"))) removeUntracked(root, name, s.path);
2465
+ const c = removeSkill(root, name, { force: true });
2466
+ if (c.action === "skipped") return failed(`${name}: ${c.reason}${from.length ? `; removed from ${from.join(", ")} so far` : ""}`);
2467
+ const repo = [...roots].find(([, r2]) => r2 === root)?.[0] ?? basename(root);
2468
+ from.push(repo);
2469
+ if (c.reason) left.push(`in ${repo}, ${c.reason}`);
2395
2470
  }
2396
2471
  const r = deleteLibrarySkill(name);
2397
2472
  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)`;
2473
+ return `Deleted ${name} from your library${from.length ? ` and from ${from.join(", ")}` : ""} (in Settings \u203A Backups)${left.length ? `; ${left.join("; ")}` : ""}`;
2399
2474
  },
2400
2475
  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}`);
2476
+ const done = movedTogether(backups, backups[i]).map((b) => [b, b.kind === "plugin" ? restorePlugin(b) : restoreBackup(b)]);
2477
+ const bad = done.filter(([, r]) => !r.ok);
2478
+ if (bad.length) return failed(bad.map(([b, r]) => `${b.name}: ${!r.ok ? r.reason : ""}`).join("; "));
2479
+ return done.map(([b, r]) => `Restored ${b.name} to ${r.ok ? tildify(r.to) : ""}`).join(" \xB7 ");
2404
2480
  },
2405
2481
  setAgents: (ids) => (setHarnesses(ids), "Saved your agents"),
2406
2482
  installVersion: (repo, name, version) => withLinks(addSkill(rootOf(repo), name, { version, force: true }), repo, name),
@@ -2409,28 +2485,28 @@ function realOps(roots, backups, machine, libraryNames) {
2409
2485
  addRoot: (path) => {
2410
2486
  const root = addRoot(path);
2411
2487
  discoverProjects();
2412
- return `Scanning ${tildify2(root)} for repos`;
2488
+ return `Scanning ${tildify(root)} for repos`;
2413
2489
  },
2414
- removeRoot: (path) => (removeRoot(path), `Stopped scanning ${tildify2(path)}; repos already found stay until you hide them`),
2490
+ removeRoot: (path) => (removeRoot(path), `Stopped scanning ${tildify(path)}; repos already found stay until you hide them`),
2415
2491
  rescan: () => (discoverProjects(), "Looked for repos in your project folders again"),
2416
2492
  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`),
2493
+ unhide: (path) => (setHidden(path, false), `${tildify(path)} is back in the list`),
2418
2494
  openFolder: (path) => {
2419
2495
  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)}`;
2496
+ const r = spawnSync3(cmd, [path], { stdio: "ignore", timeout: 1e4 });
2497
+ 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
2498
  },
2423
2499
  copy: (text, what) => {
2424
2500
  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)}`;
2501
+ 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
2502
  },
2427
2503
  installAgentSkill: () => {
2428
2504
  const r = installAgentSkill();
2429
- return r.ok ? `Your agents can now use skilllib (${r.dirs.map(tildify2).join(", ")})` : failed(`skilllib skill: ${r.reason}`);
2505
+ return r.ok ? `Your agents can now use skilllib (${r.dirs.map(tildify).join(", ")})` : failed(`skilllib skill: ${r.reason}`);
2430
2506
  },
2431
2507
  removeAgentSkill: () => {
2432
2508
  const removed = removeAgentSkill();
2433
- return removed.length ? `Removed the skilllib skill (${removed.map(tildify2).join(", ")})` : failed("The skilllib skill isn't installed");
2509
+ return removed.length ? `Removed the skilllib skill (${removed.map(tildify).join(", ")})` : failed("The skilllib skill isn't installed");
2434
2510
  },
2435
2511
  usageIssues: ({ usage, totals = {} }) => {
2436
2512
  const repoOf = new Map([...roots].map(([name, root]) => [root, name]));
@@ -2447,6 +2523,7 @@ function realOps(roots, backups, machine, libraryNames) {
2447
2523
  // src/tui/index.tsx
2448
2524
  import { jsx as jsx3 } from "react/jsx-runtime";
2449
2525
  async function tui() {
2526
+ discoverProjects();
2450
2527
  const instance = render(/* @__PURE__ */ jsx3(App, { initial: loadWorld(), reload: loadWorld, loadUsage }), { alternateScreen: true, exitOnCtrlC: true });
2451
2528
  await instance.waitUntilExit();
2452
2529
  }