skilllib 1.7.0 → 1.11.0

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.
@@ -12,11 +12,13 @@ import {
12
12
  copyLabel,
13
13
  createSkill,
14
14
  cursorPluginSkills,
15
+ cursorPlugins,
15
16
  deleteGlobal,
16
17
  deleteLibrarySkill,
17
18
  describeStep,
18
19
  discoverProjects,
19
20
  enabledHarnesses,
21
+ forgetClaudeBinary,
20
22
  forgetLatest,
21
23
  gitInfo,
22
24
  gitVisibleSteps,
@@ -24,6 +26,7 @@ import {
24
26
  harnessesChosen,
25
27
  importSkill,
26
28
  installAgentSkill,
29
+ installedPlugins,
27
30
  keptGlobal,
28
31
  latestVersion,
29
32
  libraryOrigins,
@@ -33,6 +36,7 @@ import {
33
36
  linkGlobal,
34
37
  listBackups,
35
38
  machineSkills,
39
+ moveGlobalSkills,
36
40
  planGlobalTidy,
37
41
  planProjectTidy,
38
42
  pluginBackups,
@@ -44,7 +48,6 @@ import {
44
48
  readManifest,
45
49
  readSkillInfo,
46
50
  recordOrigin,
47
- recordRemovedPlugin,
48
51
  relativeTo,
49
52
  relinkDependency,
50
53
  removeAgentSkill,
@@ -57,18 +60,20 @@ import {
57
60
  setHarnesses,
58
61
  setHidden,
59
62
  setKeepGlobal,
60
- setPluginEnabled,
63
+ setPluginOn,
61
64
  skilllibHome,
62
65
  skillsLoadedIn,
63
66
  syncProject,
64
67
  tildify,
65
68
  turnOffIn,
69
+ uninstallPlugin,
66
70
  unloadGlobal,
71
+ updatePlugin,
67
72
  updateProject,
68
73
  versionHistory,
69
74
  visibilityOf,
70
75
  visibleProjects
71
- } from "./chunk-AYAN7Q2X.js";
76
+ } from "./chunk-LO4FMXDE.js";
72
77
 
73
78
  // src/tui/index.tsx
74
79
  import { render } from "ink";
@@ -240,7 +245,7 @@ function buildReviewPrompt(skills, ctx) {
240
245
  - A skill is a folder with a SKILL.md. Every loaded skill's name and description sit in the agent's context in every session, so each one costs context and can trigger when it shouldn't.
241
246
  - "Global" skills load in EVERY repo for the listed agents. Project skills only load inside that repo.
242
247
  - I use these agents: ${ctx.harnesses.join(", ") || "(none configured)"}.
243
- - I manage skills with skilllib, which can: keep a skill global; move it into specific projects (and out of global); delete it (a backup is kept); or leave vendor skills alone. Vendor skills (plugins, claude.ai, Cursor built-ins) can only be turned off at their source.
248
+ - I manage skills with skilllib, which can: keep a skill global; move it into specific projects (and out of global); delete it (a backup is kept); or leave vendor skills alone. Vendor skills (plugins, claude.ai, Cursor built-ins, Grok bundled skills) can only be turned off at their source.
244
249
  - Usage counts only cover Claude Code sessions over the last ${ctx.usageDays} days; other agents' usage isn't tracked, so treat "unused" as a strong hint, not proof.
245
250
 
246
251
  ## My projects
@@ -543,7 +548,7 @@ function issuesOf(w, projectName, u) {
543
548
  title: `Same name as a skill in plugin ${vendor.where}: Claude Code has it off in ${projectName}, but Cursor ignores repo settings and lists both`,
544
549
  short: "Cursor also lists a plugin's copy",
545
550
  decision: true,
546
- fixes: [replacePluginFix(w, vendor.where)]
551
+ fixes: [pluginFixFor(w, vendor.where)]
547
552
  });
548
553
  else if (vendor)
549
554
  issues.push({
@@ -745,25 +750,117 @@ function deleteLibraryFix(w, names) {
745
750
  run: (w2) => joined(names.map((n) => w2.ops.deleteLibrary(n)))
746
751
  };
747
752
  }
748
- function replacePluginFix(w, id) {
749
- const skills = w.machine.filter((m) => m.source === "plugin" && m.where === id);
750
- const names = skills.map((m) => m.name);
751
- const parts = skills[0]?.pluginParts ?? [];
752
- const synced = id.endsWith("@synced");
753
+ var plural = (n, what) => `${n} ${what}${n === 1 ? "" : "s"}`;
754
+ function pluginName(p) {
755
+ return p.id.split("@")[0];
756
+ }
757
+ function pluginSkillKey(p, skill) {
758
+ return `${pluginName(p)}:${skill}`;
759
+ }
760
+ function pluginUses(w, p, repo) {
761
+ return p.skills.reduce((n, s) => n + (repo ? w.usage[repo]?.[pluginSkillKey(p, s.name)] ?? 0 : totalUses(w, pluginSkillKey(p, s.name))), 0);
762
+ }
763
+ function pluginScope(p) {
764
+ return p.repo ? `${p.scope} ${p.repo}` : p.scope;
765
+ }
766
+ function replacePluginFix(p) {
767
+ const names = p.skills.map((s) => s.name);
768
+ const synced = p.scope === "claude.ai";
769
+ const offOnly = synced || !!p.settingsOnly;
753
770
  return {
754
- label: `Replace ${id.split("@")[0]} with library skills\u2026`,
771
+ label: `Replace ${pluginName(p)} with library skills\u2026`,
755
772
  preview: [
756
- `Copies its ${skills.length} skill${skills.length === 1 ? "" : "s"} into your library and adds them to the repos you tick (or none),`,
757
- synced ? `then turns ${id} off in Claude Code. It's synced from your claude.ai account: to delete it for good, remove it there.` : `then uninstalls ${id} from Claude Code (its saved data is kept).`,
758
- parts.length ? `It also brings ${parts.join(", ")}: those ${synced ? "stop" : "go"} too.` : "",
759
- synced ? "Turn it back on with /plugin." : "Reinstall it anytime with /plugin."
773
+ `Copies its ${plural(names.length, "skill")} into your library and adds them to the repos you tick (or none),`,
774
+ 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).`,
775
+ p.parts.length ? `It also brings ${p.parts.join(", ")}: those ${offOnly ? "stop" : "go"} too.` : "",
776
+ offOnly ? "Turn it back on from Plugins." : "Settings \u203A Backups reinstalls it."
760
777
  ].filter(Boolean).join(" "),
761
778
  run: () => "",
762
779
  allowNone: true,
763
- preticked: (w2) => w2.projects.filter((p) => names.some((n) => (w2.usage[p.name]?.[n] ?? 0) > 0)).map((p) => p.name),
764
- pickRepos: (w2, repos) => w2.ops.replacePlugin(id, repos)
780
+ preticked: (w) => w.projects.filter((x) => pluginUses(w, p, x.name) > 0).map((x) => x.name),
781
+ pickRepos: (w, repos) => w.ops.replacePlugin(p, repos)
782
+ };
783
+ }
784
+ function pluginFixFor(w, id) {
785
+ const ofId = w.plugins.filter((p2) => p2.agent === "claude-code" && p2.id === id);
786
+ const p = ofId.find((x) => !x.repo) ?? ofId[0];
787
+ if (p) return replacePluginFix(p);
788
+ return { label: "Turn the plugin off with /plugin in Claude Code", preview: `skilllib can't tell where ${id} is installed.`, run: () => `Turn ${id} off with /plugin in Claude Code; skilllib picks it up next time` };
789
+ }
790
+ function pluginSwitchFix(p, on) {
791
+ const where = p.repo ? ` in ${p.repo} (${p.scope} install)` : p.scope === "claude.ai" ? " on this machine" : " in every repo";
792
+ return {
793
+ label: on ? "Turn it on" : "Turn it off",
794
+ preview: `Turns ${p.id} ${on ? "on" : "off"}${where}${p.parts.length ? `, with its ${p.parts.join(", ")}` : ""}. ${p.scope === "claude.ai" ? `Sets "${p.id}": ${on} in ~/.claude/settings.json.` : `Runs claude plugin ${on ? "enable" : "disable"} ${p.id} --scope ${p.scope}${p.repo ? ` in ${p.repo}` : ""}.`} Claude Code picks it up in its next session.`,
795
+ run: (w) => w.ops.setPlugin(p, on)
796
+ };
797
+ }
798
+ function uninstallPluginFix(p) {
799
+ return {
800
+ label: "Uninstall",
801
+ preview: `Runs claude plugin uninstall ${p.id} --keep-data --scope ${p.scope}${p.repo ? ` in ${p.repo}` : ""}: its ${[plural(p.skills.length, "skill"), ...p.parts].join(", ")} go, its saved data stays. Settings \u203A Backups reinstalls it.`,
802
+ run: (w) => w.ops.uninstallPlugin(p)
765
803
  };
766
804
  }
805
+ function updatePluginFix(p) {
806
+ return {
807
+ label: p.update === "newer" ? "Update it" : `Update to ${p.update}`,
808
+ preview: `Runs claude plugin update ${p.id}${p.repo ? ` in ${p.repo}` : ""}. Updates can bring new commands, hooks or MCP servers. If the marketplace asks to run a command, skilllib stops and you update it yourself. Claude Code uses the new version after a restart.`,
809
+ run: (w) => w.ops.updatePlugin(p)
810
+ };
811
+ }
812
+ function pluginIssues(w, p) {
813
+ const library = new Set(w.library.map((l) => l.name));
814
+ const global = new Set(w.machine.filter((m) => m.source === "global" && !m.broken).map((m) => m.name));
815
+ if (p.agent === "cursor") {
816
+ const dupes = p.skills.filter((s) => library.has(s.name) || global.has(s.name));
817
+ return dupes.length ? [
818
+ {
819
+ id: `plugin-dupes:${p.key}`,
820
+ severity: "warning",
821
+ title: `Repeats your ${dupes.map((s) => s.name).join(", ")}: Cursor lists both while it's on`,
822
+ short: `Repeats ${dupes.length} of yours`,
823
+ decision: true,
824
+ fixes: [{ label: "Turn it off in Cursor (Settings \u203A Plugins)", preview: "skilllib can't read or change Cursor's plugins.", run: () => `Turn ${p.id} off in Cursor; skilllib can't tell whether it's on` }]
825
+ }
826
+ ] : [];
827
+ }
828
+ const issues = [];
829
+ if (p.on) {
830
+ const repo = p.repo ? w.projects.find((x) => x.name === p.repo) : void 0;
831
+ const here = repo ? new Set(repo.skills.map((s) => s.name)) : void 0;
832
+ const yours = (n) => global.has(n) || (p.repo ? !!here?.has(n) : library.has(n));
833
+ const dupes = p.skills.filter((s) => yours(s.name));
834
+ if (dupes.length)
835
+ issues.push({
836
+ id: `plugin-dupes:${p.key}`,
837
+ severity: "warning",
838
+ title: `Repeats your ${dupes.map((s) => s.name).join(", ")}`,
839
+ short: `Repeats ${dupes.length} of yours`,
840
+ decision: true,
841
+ fixes: [replacePluginFix(p), pluginSwitchFix(p, false)]
842
+ });
843
+ if (w.days && p.skills.length && pluginUses(w, p) === 0)
844
+ issues.push({
845
+ id: `plugin-unused:${p.key}`,
846
+ severity: "hint",
847
+ title: `Its skills weren't used in 30 days (Claude Code)${p.parts.length ? `; it also brings ${p.parts.join(", ")}` : ""}`,
848
+ short: "Unused 30 days",
849
+ decision: true,
850
+ fixes: [pluginSwitchFix(p, false), ...p.scope === "claude.ai" || p.settingsOnly ? [] : [uninstallPluginFix(p)]]
851
+ });
852
+ }
853
+ if (p.update)
854
+ issues.push({
855
+ id: `plugin-update:${p.key}`,
856
+ severity: "hint",
857
+ title: p.update === "newer" ? "Its marketplace has a newer commit" : `Update available: ${p.version ?? "?"} \u2192 ${p.update}`,
858
+ short: p.update === "newer" ? "Update available" : `Update to ${p.update}`,
859
+ decision: true,
860
+ fixes: [updatePluginFix(p)]
861
+ });
862
+ return issues;
863
+ }
767
864
  function originGroup(origin) {
768
865
  if (!origin) return null;
769
866
  const plugin = origin.match(/^plugin: (.+)$/);
@@ -777,17 +874,21 @@ function originGroup(origin) {
777
874
  return null;
778
875
  }
779
876
  var word = (name) => name.split(/[-_.]/)[0];
877
+ function libraryOrigin(w, l) {
878
+ return l && (l.origin ?? w.library.find((x) => x.origin?.match(/^plugin: ([^@]+)@/)?.[1] === l.name)?.origin);
879
+ }
780
880
  function groupCandidates(w, u) {
781
881
  const byTime = (family, installed) => installed ? [{ key: `time:${family}:${installed}`, label: `installed together ${installed.slice(0, 10)}` }] : [];
782
882
  const byWord = (family, name) => word(name).length >= 3 ? [{ key: `word:${family}:${word(name)}`, label: `${word(name)}-*` }] : [];
783
883
  if (!("source" in u)) {
784
- const origin = originGroup(u.origin);
884
+ const origin = originGroup(libraryOrigin(w, u));
785
885
  return [...origin ? [origin] : [], ...byWord("library", u.name)];
786
886
  }
787
887
  const m = u.machine;
788
888
  if (m?.source === "plugin") return [{ key: `plugin:${m.where}`, label: `\u29C9 ${m.where.split("@")[0]} plugin` }];
789
889
  if (m?.source === "claude.ai") return [{ key: "claude.ai", label: "claude.ai skills" }];
790
890
  if (m?.source === "cursor") return [{ key: "cursor", label: "Cursor built-ins" }];
891
+ if (m?.source === "grok") return [{ key: "grok", label: "Grok bundled" }];
791
892
  if (m?.source === "system") return [{ key: `system:${m.where}`, label: m.where }];
792
893
  if (m?.source === "skilllib") return [];
793
894
  if (m) {
@@ -795,8 +896,7 @@ function groupCandidates(w, u) {
795
896
  return [...origin ? [origin] : [], ...byTime("global", m.installed), ...byWord("global", m.name)];
796
897
  }
797
898
  if (u.local?.source === "lib") {
798
- const l = w.library.find((x) => x.name === u.name);
799
- const origin = originGroup(l?.origin);
899
+ const origin = originGroup(libraryOrigin(w, w.library.find((x) => x.name === u.name)));
800
900
  return [...origin ? [origin] : [], ...byWord("local", u.name)];
801
901
  }
802
902
  return byWord("local", u.name);
@@ -823,7 +923,33 @@ function addRows(w, target, q) {
823
923
  const taken = new Set(target ? project(w, target).skills.map((s) => s.name) : []);
824
924
  const loaded = new Set(target ? usable(w, target).map((u) => u.name) : []);
825
925
  const hit = (name, description) => !q || matches(name, q) || q.length >= 3 && description.toLowerCase().includes(q);
826
- const library = target ? w.library.filter((l) => !taken.has(l.name) && hit(l.name, w.descriptions[l.name] ?? "")).map((l) => ({ key: `lib:${l.name}`, name: l.name, note: `v${l.latest}`, description: w.descriptions[l.name] ?? "", run: (w2) => w2.ops.add(target, l.name) })) : [];
926
+ const groups = assignGroups(w.library.map((l) => groupCandidates(w, l)));
927
+ const shown = w.library.map((l, i) => ({ l, g: groups[i] })).filter(({ l }) => !taken.has(l.name) && hit(l.name, w.descriptions[l.name] ?? ""));
928
+ const skillRow = (l, inGroup) => ({
929
+ key: `lib:${l.name}`,
930
+ name: l.name,
931
+ note: `v${l.latest}`,
932
+ description: w.descriptions[l.name] ?? "",
933
+ members: [l.name],
934
+ ...inGroup ? { inGroup } : {},
935
+ run: (w2) => w2.ops.add(target, l.name)
936
+ });
937
+ const library = [];
938
+ const done = /* @__PURE__ */ new Set();
939
+ for (const { l, g } of target ? shown : []) {
940
+ if (done.has(l.name)) continue;
941
+ const members = g ? shown.filter((x) => x.g?.key === g.key).map((x) => x.l) : [l];
942
+ members.forEach((m) => done.add(m.name));
943
+ if (members.length < 2) {
944
+ library.push(skillRow(l, false));
945
+ continue;
946
+ }
947
+ const names = members.map((m) => m.name);
948
+ library.push(
949
+ { key: `group:${g.key}`, name: groupLabel(g, names), note: `${names.length} skills`, description: names.join(", "), members: names, run: (w2) => w2.ops.addTo([target], names) },
950
+ ...members.map((m) => skillRow(m, true))
951
+ );
952
+ }
827
953
  const exists = q !== "" && (loaded.has(q) || w.library.some((l) => l.name === q));
828
954
  const where = target ? ` for ${target}` : "";
829
955
  const fresh = exists ? [] : [
@@ -918,6 +1044,7 @@ var PLACES = [
918
1044
  ["projects", "Projects"],
919
1045
  ["library", "Your skills"],
920
1046
  ["global", "Global"],
1047
+ ["plugins", "Plugins"],
921
1048
  ["health", "Health"],
922
1049
  ["settings", "Settings"]
923
1050
  ];
@@ -996,7 +1123,7 @@ function TitleRow({ cells, width }) {
996
1123
  }
997
1124
  var fixOption = (fix) => ({ label: fix.label, fix });
998
1125
  var GIT_LABEL = { committed: "\u2713 committed", changed: "\xB1 uncommitted changes", new: "+ not committed", ignored: "\u2205 gitignored" };
999
- var COPY_RANK = { lib: 0, repo: 0, untracked: 0, global: 1, skilllib: 1, plugin: 2, "claude.ai": 3, cursor: 3, system: 3 };
1126
+ var COPY_RANK = { lib: 0, repo: 0, untracked: 0, global: 1, skilllib: 1, plugin: 2, "claude.ai": 3, cursor: 3, grok: 3, system: 3 };
1000
1127
  function byName(copies) {
1001
1128
  const groups = /* @__PURE__ */ new Map();
1002
1129
  for (const c of copies) groups.set(c.name, [...groups.get(c.name) ?? [], c]);
@@ -1139,11 +1266,13 @@ function overview(w, p) {
1139
1266
  const project2 = project(w, p);
1140
1267
  const info = w.ops.repoInfo(p);
1141
1268
  const manifest = project2.manifest === "none" ? ["no skilllib.json", color.faint] : project2.manifest === "no git" ? ["skilllib.json (not a git repo)", color.muted] : [`skilllib.json ${GIT_LABEL[project2.manifest]}`, project2.manifest === "committed" ? color.text : color.yellow];
1269
+ const plugins = [...new Set(machineIn(w, project2).filter((m) => m.source === "plugin").map((m) => m.where.split("@")[0]))];
1142
1270
  return [
1143
1271
  ["Folder", [[tildify3(project2.path), color.text]]],
1144
1272
  ["Remote", [[info.remote ?? "none", info.remote ? color.blue : color.faint]]],
1145
1273
  ["Branch", [[info.branch ?? "\u2013", color.text], [info.dirty ? ` ${info.dirty} uncommitted` : "", color.yellow]]],
1146
- ["Manifest", [manifest]]
1274
+ ["Manifest", [manifest]],
1275
+ ["Plugins", [[plugins.length ? plugins.join(", ") : "none", plugins.length ? color.magenta : color.faint]]]
1147
1276
  ];
1148
1277
  }
1149
1278
  function InfoGrid({ rows, columns, width }) {
@@ -1185,13 +1314,21 @@ function globalItems(w, nameW, ui) {
1185
1314
  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 }));
1186
1315
  return byName(copies).map((g) => skillItem(w, g, mergeIssues(g.map((c) => machineIssues(w, c.machine))), nameW, globalActions(w, g[0], ui), null));
1187
1316
  }
1317
+ var AT_SOURCE = {
1318
+ global: null,
1319
+ plugin: null,
1320
+ skilllib: null,
1321
+ "claude.ai": "Turn it off in claude.ai \u203A Settings",
1322
+ cursor: "Cursor manages it",
1323
+ grok: "Ships with Grok; a same-named skill of yours overrides it"
1324
+ };
1188
1325
  function globalActions(w, u, ui) {
1189
1326
  const m = u.machine;
1190
- const atSource = m.source === "plugin" ? "Turn it off with /plugin in Claude Code" : m.source === "claude.ai" ? "Turn it off in claude.ai \u203A Settings" : m.source === "system" ? `An admin manages ${m.where}` : "Cursor manages it";
1327
+ const atSource = m.source === "system" ? `An admin manages ${m.where}` : AT_SOURCE[m.source];
1191
1328
  return [
1192
1329
  ...machineActions(m).map(fixOption),
1193
- ...m.source === "plugin" ? [fixOption(replacePluginFix(w, m.where))] : [],
1194
- ...m.source === "global" || m.source === "skilllib" ? [] : [{ label: atSource, action: () => ui.notify(`${atSource}; skilllib picks up the change next time`) }],
1330
+ ...m.source === "plugin" ? [{ label: `Open ${m.where.split("@")[0]} in Plugins`, action: () => ui.openInPlugins(m.where) }] : [],
1331
+ ...atSource ? [{ label: atSource, action: () => ui.notify(`${atSource}; skilllib picks up the change next time`) }] : [],
1195
1332
  ...m.source === "global" && !m.broken ? [{ label: "Edit SKILL.md", action: () => ui.edit(`${m.path}/SKILL.md`) }] : [],
1196
1333
  reviewOption(w, m.name, ui)
1197
1334
  ];
@@ -1283,6 +1420,66 @@ function agentSkillItems(w, nameW) {
1283
1420
  }
1284
1421
  ];
1285
1422
  }
1423
+ var pluginW = (nameW) => Math.min(nameW, 17);
1424
+ var STATE = { on: ["on", color.green], off: ["off", color.faint], unknown: ["?", color.faint] };
1425
+ function pluginItems(w, skillW, ui, issuesOf2) {
1426
+ const nameW = pluginW(skillW);
1427
+ return w.plugins.map((p) => {
1428
+ const issues = issuesOf2.get(p.key) ?? [];
1429
+ const top = worst(issues);
1430
+ const cursor = p.agent === "cursor";
1431
+ const uses = cursor ? 0 : pluginUses(w, p);
1432
+ const [state, stateColor] = STATE[p.on === null ? "unknown" : p.on ? "on" : "off"];
1433
+ const n = p.skills.length;
1434
+ const name = pluginName(p);
1435
+ const actions = cursor ? [{ label: "Turn it off in Cursor (Settings \u203A Plugins)", action: () => ui.notify(`Turn ${p.id} off in Cursor; skilllib can't read or change Cursor's plugins`) }] : [
1436
+ fixOption(pluginSwitchFix(p, !p.on)),
1437
+ ...p.update ? [fixOption(updatePluginFix(p))] : [],
1438
+ ...n ? [fixOption(replacePluginFix(p))] : [],
1439
+ ...p.scope === "claude.ai" || p.settingsOnly ? [] : [fixOption(uninstallPluginFix(p))]
1440
+ ];
1441
+ return {
1442
+ key: p.key,
1443
+ name: p.id,
1444
+ plugin: p,
1445
+ issues,
1446
+ uses,
1447
+ cells: [
1448
+ { text: top ? SEV[top.severity].icon : " ", width: 2, color: top ? SEV[top.severity].color : void 0 },
1449
+ { text: clip(name, nameW), width: nameW, color: p.on === false ? color.muted : void 0 },
1450
+ { text: harness(p.agent).icon, width: 3, color: harness(p.agent).color },
1451
+ { text: clip(cursor ? "\u2013" : pluginScope(p), 19), width: 19, color: p.repo ? color.blue : color.muted },
1452
+ { text: clip(`${n} skill${n === 1 ? "" : "s"}${p.parts.length ? ` +${p.parts.length}` : ""}`, 13), width: 13, color: color.muted },
1453
+ { text: state.padEnd(4), width: 4, color: stateColor },
1454
+ { text: cursor ? " \u2013 " : usesText(uses), width: 6, color: color.muted },
1455
+ { text: " " + (top?.short ?? ""), grow: true, color: top ? SEV[top.severity].color : color.faint }
1456
+ ],
1457
+ detail: (width) => {
1458
+ const b = builder(p.id);
1459
+ for (const l of wrap(p.description || "(no description)", width - 4, 2)) b.line([l, color.muted]);
1460
+ b.line();
1461
+ b.issues(issues);
1462
+ b.actions([...actions, { label: "Open its folder", action: () => ui.notify(said2(w.ops.openFolder(p.path))) }, {
1463
+ label: "Review prompt",
1464
+ action: () => ui.copy(
1465
+ reviewPrompt(w, p.skills.map((s) => s.name), `Review the ${harness(p.agent).name} plugin ${p.id}${p.description ? ` (${p.description})` : ""}: these are its skills${p.parts.length ? `, and it also brings ${p.parts.join(", ")}` : ""}. Should I keep it ${p.on === false ? "off" : "on"}, turn it ${p.on === false ? "on" : "off"}, uninstall it, or replace it with my own copies of the skills I use?`),
1466
+ `a review prompt for ${name}`
1467
+ )
1468
+ }], width);
1469
+ b.line();
1470
+ const label = (t) => [t.padEnd(12), color.muted];
1471
+ const uses2 = (n2) => n2 ? `${n2} use${n2 === 1 ? "" : "s"}` : "unused";
1472
+ const skills = p.skills.map((s) => cursor || !w.days ? s.name : `${s.name} (${uses2(totalUses(w, pluginSkillKey(p, s.name)))})`);
1473
+ wrap(skills.join(" \xB7 ") || "none", width - 16, 4).forEach((l, i) => b.line(label(i ? "" : "Skills"), [l, color.text]));
1474
+ b.line(label("Also brings"), [p.parts.join(", ") || "nothing else", p.parts.length ? color.text : color.faint]);
1475
+ 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`;
1476
+ 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]);
1477
+ b.line(label("Folder"), [tildify3(p.path), color.faint]);
1478
+ return b.d;
1479
+ }
1480
+ };
1481
+ });
1482
+ }
1286
1483
  function addCursor(rows, cursor) {
1287
1484
  const i = Math.min(cursor, rows.length - 1);
1288
1485
  return rows[i] && !rows[i].header ? i : Math.max(0, rows.findIndex((r) => !r.header));
@@ -1324,10 +1521,12 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1324
1521
  const ui = {
1325
1522
  notify: setToast,
1326
1523
  openInGlobal,
1524
+ openInPlugins,
1327
1525
  edit,
1328
1526
  copy: (text, what) => setToast(said2(world.ops.copy(text, what))),
1329
1527
  menu: (title, options) => setModal({ kind: "menu", title, options, cursor: 0 })
1330
1528
  };
1529
+ const pluginIssues2 = new Map(world.plugins.map((p) => [p.key, pluginIssues(world, p)]));
1331
1530
  const flagged = [
1332
1531
  ...world.projects.flatMap((p) => projectIssues(world, p.name).map((x) => ({ key: `${p.name}:${x.skill.name}`, issue: x.issue }))),
1333
1532
  ...world.machine.flatMap((m) => machineIssues(world, m).map((issue) => ({ key: `Global:${m.name}`, issue }))),
@@ -1336,7 +1535,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1336
1535
  ];
1337
1536
  const healthCount = new Set(flagged.map((f) => f.key)).size;
1338
1537
  const healthWorst = worst(flagged.map((f) => f.issue));
1339
- const all = place === "projects" ? open ? projectSkillItems(world, open, nameW, ui) : projectItems(world) : place === "global" ? globalItems(world, nameW, ui) : place === "library" ? libraryItems(world, nameW, ui) : place === "health" ? healthItems(world, nameW, ui) : settingsItems();
1538
+ const all = place === "projects" ? open ? projectSkillItems(world, open, nameW, ui) : projectItems(world) : place === "global" ? globalItems(world, nameW, ui) : place === "library" ? libraryItems(world, nameW, ui) : place === "plugins" ? pluginItems(world, nameW, ui, pluginIssues2) : place === "health" ? healthItems(world, nameW, ui) : settingsItems();
1340
1539
  const inChip = (i, c) => c === "all" || (c === "issues" ? i.issues.length > 0 : i.group === c);
1341
1540
  let items = all.filter((i) => i.header || (!query || matches(i.name, query)) && inChip(i, chip));
1342
1541
  const viewKey = [place, open, chip, query, used ? "usage" : ""].join("|");
@@ -1485,8 +1684,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1485
1684
  ] : [],
1486
1685
  ...libs.length ? [fixOption(deleteLibraryFix(world, libs.map((l) => l.name)))] : [],
1487
1686
  ...locals.length ? [fixOption({ label: `Remove all ${locals.length} from this repo`, preview: `Remove these ${n(locals.length)} from ${open}; your library keeps the tracked ones.`, run: (w) => (locals.forEach((s) => w.ops.remove(open, s.name)), `Removed ${n(locals.length)} from ${open}`) })] : [],
1488
- ...[...new Set(machines.filter((m) => m.source === "plugin").map((m) => m.where))].map((id) => fixOption(replacePluginFix(world, id))),
1489
- ...machines.some((m) => m.source === "plugin") ? [{ label: "Turn the plugin off with /plugin in Claude Code", action: () => setToast("Turn it off with /plugin in Claude Code; skilllib picks it up next time") }] : [],
1687
+ ...[...new Set(machines.filter((m) => m.source === "plugin").map((m) => m.where))].map((id) => ({ label: `Open ${id.split("@")[0]} in Plugins`, action: () => openInPlugins(id) })),
1490
1688
  { label: "Review prompt", action: () => ui.copy(reviewPrompt(world, names, `Review these related skills (${label}).`), `a review prompt for ${label}`) }
1491
1689
  ];
1492
1690
  }
@@ -1513,7 +1711,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1513
1711
  const repo = open;
1514
1712
  const skills = project(world, repo).skills.map((s) => s.name);
1515
1713
  return [
1516
- row("#add", "+ Add a skill\u2026", ["Search your library, or create a new skill."], () => setModal({ kind: "add", cursor: 0, query: "", target: repo })),
1714
+ row("#add", "+ Add a skill\u2026", ["Search your library, or create a new skill."], () => setModal({ kind: "add", cursor: 0, query: "", target: repo, picked: /* @__PURE__ */ new Set() })),
1517
1715
  ...fix,
1518
1716
  row(
1519
1717
  "#repo",
@@ -1536,7 +1734,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1536
1734
  }
1537
1735
  if (place === "health") return fix;
1538
1736
  if (place === "library")
1539
- return [row("#add", "+ New skill\u2026", ["Create a skill in your library, or have an agent write it. Skills in your repos can be copied in from their own actions."], () => setModal({ kind: "add", cursor: 0, query: "", target: null }))];
1737
+ return [row("#add", "+ New skill\u2026", ["Create a skill in your library, or have an agent write it. Skills in your repos can be copied in from their own actions."], () => setModal({ kind: "add", cursor: 0, query: "", target: null, picked: /* @__PURE__ */ new Set() }))];
1540
1738
  return [];
1541
1739
  }
1542
1740
  function move(delta) {
@@ -1579,6 +1777,10 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1579
1777
  goPlace("global");
1580
1778
  setQuery(name);
1581
1779
  }
1780
+ function openInPlugins(id) {
1781
+ goPlace("plugins");
1782
+ setQuery(id);
1783
+ }
1582
1784
  function settingsItems() {
1583
1785
  const header = (key, text) => ({ key, name: "", header: true, issues: [], uses: 0, cells: [{ text }] });
1584
1786
  const row = (key, text, sub, onEnter, c) => ({
@@ -1790,6 +1992,18 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1790
1992
  if (key.upArrow) return step(-1);
1791
1993
  if (key.downArrow) return step(1);
1792
1994
  if (key.backspace || key.delete) return setModal({ ...m, query: m.query.slice(0, -1), cursor: 0 });
1995
+ const members = rows[cursor2]?.members;
1996
+ if (input === " " && members) {
1997
+ const picked = new Set(m.picked);
1998
+ const all2 = members.every((n) => picked.has(n));
1999
+ for (const n of members) all2 ? picked.delete(n) : picked.add(n);
2000
+ return setModal({ ...m, picked });
2001
+ }
2002
+ if (key.return && m.target && m.picked.size) {
2003
+ const [target, names] = [m.target, [...m.picked]];
2004
+ setModal(null);
2005
+ return apply((w) => w.ops.addTo([target], names));
2006
+ }
1793
2007
  if (key.return && rows[cursor2]?.agent) {
1794
2008
  setModal(null);
1795
2009
  return ui.copy(writeSkillPrompt(world, m.query, m.target), "a prompt for an agent to write the skill");
@@ -1812,6 +2026,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1812
2026
  const globalFlagged = world.machine.map((m) => [m.name, machineIssues(world, m)]).filter(([, issues]) => issues.length);
1813
2027
  const globalIssues = new Set(globalFlagged.map(([name]) => name)).size;
1814
2028
  const globalProblem = globalFlagged.some(([, issues]) => issues.some((x) => x.severity === "problem"));
2029
+ const pluginsWorst = worst([...pluginIssues2.values()].flat().filter((i) => i.severity !== "hint"));
1815
2030
  const badge = (p) => {
1816
2031
  const found = projectIssues(world, p);
1817
2032
  const top = worst(found.map((x) => x.issue));
@@ -1833,6 +2048,9 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1833
2048
  sideRow("global", "\u25C8 Global", place === "global", () => goPlace("global"), [
1834
2049
  { text: String(new Set(world.machine.map((m) => m.name)).size), width: 4, color: globalProblem ? color.red : globalIssues ? color.yellow : color.muted }
1835
2050
  ]),
2051
+ sideRow("plugins", "\u29C9 Plugins", place === "plugins", () => goPlace("plugins"), [
2052
+ { text: String(world.plugins.length), width: 4, color: pluginsWorst ? SEV[pluginsWorst.severity].color : color.muted }
2053
+ ]),
1836
2054
  sideRow("health", "\u2713 Health", place === "health", () => goPlace("health"), [
1837
2055
  { text: String(healthCount), width: 4, color: healthWorst ? SEV[healthWorst.severity].color : color.green }
1838
2056
  ]),
@@ -1870,17 +2088,17 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1870
2088
  }),
1871
2089
  /* @__PURE__ */ jsx2(Text2, { color: color.accent, children: query ? ` \u2315 ${query}\u258F` : "" })
1872
2090
  ] }) });
1873
- const columnTitles = dashboard || place === "global" ? titles([["", 2], ["Skill", nameW], ["Source", 14], [" Uses", 6], [" Issue", 0]]) : place === "projects" ? titles([["", 2], ["Project", 18], ["Skills", 10], ["Issues", 0]]) : place === "library" ? titles([["", 2], ["Skill", nameW], ["Ver", 5], ["Used in", 18], [" Uses", 6], [" Issue / description", 0]]) : place === "health" ? titles([["", 2], ["Skill", nameW], ["Where", 16], [" Issue", 0]]) : null;
2091
+ const columnTitles = dashboard || place === "global" ? titles([["", 2], ["Skill", nameW], ["Source", 14], [" Uses", 6], [" Issue", 0]]) : place === "projects" ? titles([["", 2], ["Project", 18], ["Skills", 10], ["Issues", 0]]) : place === "library" ? titles([["", 2], ["Skill", nameW], ["Ver", 5], ["Used in", 18], [" Uses", 6], [" Issue / description", 0]]) : place === "plugins" ? titles([["", 2], ["Plugin", pluginW(nameW)], ["", 3], ["Scope", 19], ["Brings", 13], ["On", 4], [" Uses", 6], [" Issue", 0]]) : place === "health" ? titles([["", 2], ["Skill", nameW], ["Where", 16], [" Issue", 0]]) : null;
1874
2092
  const showChips = chips.length > 0;
1875
2093
  const listHeader = /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
1876
2094
  showChips ? chipRow : null,
1877
2095
  columnTitles ? /* @__PURE__ */ jsx2(TitleRow, { cells: columnTitles, width: columns }) : null
1878
2096
  ] });
1879
- const overviewH = dashboard && !sidebar ? 4 : 0;
2097
+ const overviewH = dashboard && !sidebar ? Math.ceil(overview(world, open).length / 2) + 2 : 0;
1880
2098
  const listH = split ? Math.floor((bodyH - overviewH) / 2) : bodyH - overviewH;
1881
2099
  const detailH = split ? bodyH - overviewH - listH : bodyH;
1882
2100
  const listRows = items.map((i) => ({ key: i.key, cells: i.cells, header: i.header }));
1883
- const listTitle = dashboard ? "Skills" : place === "projects" ? "Projects" : place === "library" ? "Your skills" : place === "global" ? "Loads everywhere" : place === "health" ? "To fix" : "Settings";
2101
+ const listTitle = dashboard ? "Skills" : place === "projects" ? "Projects" : place === "library" ? "Your skills" : place === "global" ? "Loads everywhere" : place === "plugins" ? "Plugins" : place === "health" ? "To fix" : "Settings";
1884
2102
  const list = /* @__PURE__ */ jsx2(
1885
2103
  ListPanel,
1886
2104
  {
@@ -1944,6 +2162,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1944
2162
  else if (modal?.kind === "repos") {
1945
2163
  const repos = (modal.only ?? world.projects.map((p) => p.name)).filter((r) => !modal.query || matches(r, modal.query));
1946
2164
  const name = current?.name ?? "";
2165
+ const usedHere = (r) => current?.plugin ? pluginUses(world, current.plugin, r) : world.usage[r]?.[name] ?? 0;
1947
2166
  const pickerLines = wrap(modal.fix.preview, columns - 6, 4);
1948
2167
  body = /* @__PURE__ */ jsx2(
1949
2168
  ListPanel,
@@ -1964,7 +2183,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1964
2183
  cells: [
1965
2184
  { text: modal.picked.has(r) ? "[x] " : "[ ] ", width: 4, color: modal.picked.has(r) ? color.green : color.faint },
1966
2185
  { text: r, width: 20 },
1967
- { text: world.usage[r]?.[name] ? `used ${world.usage[r][name]}\xD7 here` : "", grow: true, color: color.faint }
2186
+ { text: usedHere(r) ? `used ${usedHere(r)}\xD7 here` : "", grow: true, color: color.faint }
1968
2187
  ]
1969
2188
  }))
1970
2189
  }
@@ -1992,10 +2211,15 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1992
2211
  );
1993
2212
  } else if (modal?.kind === "add") {
1994
2213
  const rows = addRows(world, modal.target, modal.query);
2214
+ const picked = modal.picked;
2215
+ const tick = (names) => {
2216
+ const n = names.filter((x) => picked.has(x)).length;
2217
+ return { text: n === names.length ? "[x] " : n ? "[-] " : "[ ] ", width: 4, color: n ? color.green : color.faint };
2218
+ };
1995
2219
  body = /* @__PURE__ */ jsx2(
1996
2220
  ListPanel,
1997
2221
  {
1998
- title: modal.target ? `Add a skill to ${modal.target}` : "New skill",
2222
+ title: modal.target ? picked.size ? `Add ${picked.size} skill${picked.size === 1 ? "" : "s"} to ${modal.target}` : `Add a skill to ${modal.target}` : "New skill",
1999
2223
  focused: true,
2000
2224
  width: columns,
2001
2225
  height: bodyH,
@@ -2004,10 +2228,14 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
2004
2228
  header: /* @__PURE__ */ jsx2(Text2, { color: modal.query ? color.accent : color.muted, children: modal.query ? `\u2315 ${modal.query}\u258F` : modal.target ? "type to search your library, or to name a new skill" : "type the new skill's name" }),
2005
2229
  headerHeight: 1,
2006
2230
  rows: rows.map(
2007
- (r) => r.header ? { key: r.key, header: true, cells: [{ text: r.header }] } : r.create || r.agent ? { key: r.key, cells: [{ text: r.name, grow: true, color: color.accent }] } : {
2231
+ (r) => r.header ? { key: r.key, header: true, cells: [{ text: r.header }] } : r.create || r.agent ? { key: r.key, cells: [{ text: r.name, grow: true, color: color.accent }] } : r.members.length > 1 ? {
2232
+ key: r.key,
2233
+ cells: [tick(r.members), { text: clip(r.name, 30), width: 30, bold: true }, { text: `${r.note}: ${r.description}`, grow: true, color: color.faint }]
2234
+ } : {
2008
2235
  key: r.key,
2009
2236
  cells: [
2010
- { text: clip(r.name, 22), width: 22 },
2237
+ tick(r.members),
2238
+ { text: clip(`${r.inGroup ? " " : ""}${r.name}`, 24), width: 24 },
2011
2239
  { text: r.note, width: 6, color: color.accent },
2012
2240
  { text: r.description, grow: true, color: color.faint }
2013
2241
  ]
@@ -2022,7 +2250,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
2022
2250
  list,
2023
2251
  split ? side : null
2024
2252
  ] });
2025
- const hints = modal?.kind === "confirm" ? "enter apply \xB7 esc cancel" : modal?.kind === "repos" || modal?.kind === "fixall" ? "space tick \xB7 enter apply \xB7 esc cancel" : modal?.kind === "agents" ? "space tick \xB7 enter save" : modal?.kind === "menu" ? "\u2191\u2193 choose \xB7 enter do it \xB7 esc cancel" : modal?.kind === "add" ? "enter add \xB7 esc cancel" : modal ? "any key closes" : focus === "detail" ? "\u2191\u2193 \u2190\u2192 choose \xB7 enter apply \xB7 esc back" : focus === "sidebar" ? "\u2191\u2193 choose \xB7 enter open \xB7 type to filter repos" : place === "projects" && !open ? `type to filter \xB7 enter open \xB7 ${sidebar ? "\u2190 sidebar" : "\u2190\u2192 places"} \xB7 ? keys` : place === "settings" ? `enter toggle \xB7 ${sidebar ? "\u2190 sidebar" : "\u2190\u2192 places"} \xB7 ? keys` : `type to filter \xB7 space \u2605 fix \xB7 enter actions \xB7 tab tabs${sidebar ? " \xB7 esc back" : `${dashboard ? " \xB7 esc all projects" : ""} \xB7 ? keys`}`;
2253
+ const hints = modal?.kind === "confirm" ? "enter apply \xB7 esc cancel" : modal?.kind === "repos" || modal?.kind === "fixall" ? "space tick \xB7 enter apply \xB7 esc cancel" : modal?.kind === "agents" ? "space tick \xB7 enter save" : modal?.kind === "menu" ? "\u2191\u2193 choose \xB7 enter do it \xB7 esc cancel" : modal?.kind === "add" ? modal.target ? "space tick \xB7 enter add \xB7 esc cancel" : "enter add \xB7 esc cancel" : modal ? "any key closes" : focus === "detail" ? "\u2191\u2193 \u2190\u2192 choose \xB7 enter apply \xB7 esc back" : focus === "sidebar" ? "\u2191\u2193 choose \xB7 enter open \xB7 type to filter repos" : place === "projects" && !open ? `type to filter \xB7 enter open \xB7 ${sidebar ? "\u2190 sidebar" : "\u2190\u2192 places"} \xB7 ? keys` : place === "settings" ? `enter toggle \xB7 ${sidebar ? "\u2190 sidebar" : "\u2190\u2192 places"} \xB7 ? keys` : `type to filter \xB7 space \u2605 fix \xB7 enter actions \xB7 tab tabs${sidebar ? " \xB7 esc back" : `${dashboard ? " \xB7 esc all projects" : ""} \xB7 ? keys`}`;
2026
2254
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: termCols, height: termRows, children: [
2027
2255
  sidebar ? null : topBar,
2028
2256
  /* @__PURE__ */ jsxs2(Box2, { children: [
@@ -2100,7 +2328,7 @@ function Help({ width, height }) {
2100
2328
  }
2101
2329
 
2102
2330
  // src/tui/load.ts
2103
- import { execFileSync, spawn, spawnSync as spawnSync3 } from "child_process";
2331
+ import { execFileSync, spawn } from "child_process";
2104
2332
  import { existsSync as existsSync3, lstatSync, realpathSync } from "fs";
2105
2333
  import { basename, dirname, join as join2 } from "path";
2106
2334
  function tildify4(p) {
@@ -2155,20 +2383,6 @@ function localSkill(root, s, git2) {
2155
2383
  git: state
2156
2384
  };
2157
2385
  }
2158
- var partsOf = /* @__PURE__ */ new Map();
2159
- function pluginParts(root) {
2160
- if (!partsOf.has(root))
2161
- partsOf.set(
2162
- root,
2163
- [
2164
- ["commands", "commands"],
2165
- ["agents", "agents"],
2166
- ["hooks", "hooks"],
2167
- [".mcp.json", "MCP servers"]
2168
- ].flatMap(([file, what]) => existsSync3(join2(root, file)) ? [what] : [])
2169
- );
2170
- return partsOf.get(root);
2171
- }
2172
2386
  function installedAt(path) {
2173
2387
  try {
2174
2388
  const st = lstatSync(path);
@@ -2182,7 +2396,7 @@ function machineSkill(s, kept) {
2182
2396
  const global = s.kind === "global" || s.kind === "skills.sh";
2183
2397
  return {
2184
2398
  name: s.name,
2185
- source: global ? "global" : s.kind === "plugin" ? "plugin" : s.kind === "claude.ai" ? "claude.ai" : s.kind === "system" ? "system" : s.kind === "skilllib" ? "skilllib" : "cursor",
2399
+ 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",
2186
2400
  where: global || s.kind === "skilllib" ? tildify4(dirname(s.path)) : s.kind === "claude.ai" ? "claude.ai account" : s.kind === "built-in" ? `${s.origin} built-in` : s.origin,
2187
2401
  ...s.kind === "skills.sh" ? { origin: `npx skills: ${s.origin}` } : {},
2188
2402
  path: s.path,
@@ -2190,10 +2404,31 @@ function machineSkill(s, kept) {
2190
2404
  agents: s.harnesses,
2191
2405
  ...global && kept.has(s.name) ? { kept: true } : {},
2192
2406
  ...s.broken ? { broken: true } : {},
2193
- ...global && s.kind !== "skills.sh" ? { installed: installedAt(s.path) } : {},
2194
- ...s.kind === "plugin" ? { pluginParts: pluginParts(dirname(dirname(s.path))) } : {}
2407
+ ...global && s.kind !== "skills.sh" ? { installed: installedAt(s.path) } : {}
2408
+ };
2409
+ }
2410
+ function pluginOf(p, repoOf) {
2411
+ const scope = p.agent === "cursor" ? "cursor" : p.synced ? "claude.ai" : p.scope ?? "user";
2412
+ const repo = p.projectPath ? repoOf(p.projectPath) ?? basename(p.projectPath) : void 0;
2413
+ return {
2414
+ key: [p.agent, p.id, scope, p.projectPath ?? ""].join("|"),
2415
+ id: p.id,
2416
+ agent: p.agent,
2417
+ scope,
2418
+ ...repo ? { repo, projectPath: p.projectPath } : {},
2419
+ on: p.on,
2420
+ ...p.version ? { version: p.version } : {},
2421
+ ...p.update ? { update: p.update } : {},
2422
+ ...p.settingsOnly ? { settingsOnly: true } : {},
2423
+ description: p.description,
2424
+ path: p.root,
2425
+ skills: p.skills.map((path) => ({ name: basename(path), path })),
2426
+ parts: p.extras
2195
2427
  };
2196
2428
  }
2429
+ function refOf(p) {
2430
+ return { id: p.id, scope: p.scope === "claude.ai" ? null : p.scope, ...p.projectPath ? { projectPath: p.projectPath } : {}, synced: p.scope === "claude.ai", ...p.settingsOnly ? { settingsOnly: true } : {} };
2431
+ }
2197
2432
  function dupesOf(r, root, git2 = null) {
2198
2433
  const out = /* @__PURE__ */ new Map();
2199
2434
  for (const p of r.plans) out.set(p.name, { steps: p.steps.map((s) => describeStep(s, root)), git: gitVisibleSteps(p, git2).map((s) => describeStep(s, root)) });
@@ -2203,6 +2438,7 @@ function dupesOf(r, root, git2 = null) {
2203
2438
  }
2204
2439
  function loadWorld() {
2205
2440
  forgetLatest();
2441
+ forgetClaudeBinary();
2206
2442
  const agents = enabledHarnesses();
2207
2443
  const here = projectHere();
2208
2444
  const real = (p) => {
@@ -2223,7 +2459,8 @@ function loadWorld() {
2223
2459
  return { name: s.name, latest: latestVersion(s.name)?.version ?? 1, origin: origins[s.name] };
2224
2460
  });
2225
2461
  const kept = keptGlobal();
2226
- const cursorPlugin = new Map(agents.includes("cursor") ? cursorPluginSkills().map((s) => [s.name, s.plugin]) : []);
2462
+ const cursorList = agents.includes("cursor") ? cursorPlugins() : [];
2463
+ const cursorPlugin = new Map(cursorPluginSkills(cursorList).map((s) => [s.name, s.plugin]));
2227
2464
  const globalDupes = dupesOf(planGlobalTidy({ enabled: agents }), null);
2228
2465
  const sourced = machineSkills(agents);
2229
2466
  const machine = sourced.map((s) => {
@@ -2234,7 +2471,7 @@ function loadWorld() {
2234
2471
  const plugin = cursorPlugin.get(m.name);
2235
2472
  return { ...m, ...dupes ? { dupes } : {}, ...plugin ? { cursorPlugin: plugin } : {} };
2236
2473
  });
2237
- const plugins = (root) => {
2474
+ const loadedHere = (root) => {
2238
2475
  const loaded = skillsLoadedIn(root, sourced, agents);
2239
2476
  if (loaded === sourced) return {};
2240
2477
  for (const s of loaded) descriptions[s.name] ??= s.description;
@@ -2255,15 +2492,18 @@ function loadWorld() {
2255
2492
  const plugin = cursorPlugin.get(s.name);
2256
2493
  return { ...localSkill(root, s, git2), ...d ? { dupes: d } : {}, ...plugin ? { cursorPlugin: plugin } : {} };
2257
2494
  }),
2258
- ...plugins(root)
2495
+ ...loadedHere(root)
2259
2496
  };
2260
2497
  });
2261
2498
  const backups = [...listBackups(), ...pluginBackups()].sort((a, b) => b.movedAt.localeCompare(a.movedAt));
2499
+ const repoOf = (path) => projects.find((p) => real(p.path) === real(path))?.name;
2500
+ const plugins = [...installedPlugins(), ...cursorList].map((p) => pluginOf(p, repoOf));
2262
2501
  return {
2263
2502
  agents,
2264
2503
  cwd: cwd ? names.get(cwd) : null,
2265
2504
  projects,
2266
2505
  machine,
2506
+ plugins,
2267
2507
  library,
2268
2508
  usage: {},
2269
2509
  descriptions,
@@ -2325,6 +2565,9 @@ function realOps(roots, backups) {
2325
2565
  };
2326
2566
  const addTo = (repos, names) => {
2327
2567
  if (repos.length === 1 && names.length === 1) return withLinks(addSkill(rootOf(repos[0]), names[0]), repos[0], names[0]);
2568
+ return addMany(repos, names).result;
2569
+ };
2570
+ const addMany = (repos, names) => {
2328
2571
  const changes = repos.flatMap((repo) => names.map((name) => ({ repo, name, c: addSkill(rootOf(repo), name) })));
2329
2572
  const done = changes.filter((x) => x.c.action !== "skipped");
2330
2573
  const added = [...new Set(done.map((x) => x.name))];
@@ -2343,9 +2586,8 @@ function realOps(roots, backups) {
2343
2586
  const skipped = changes.filter((x) => x.c.action === "skipped").map((x) => `${x.name} not added to ${x.repo}: ${x.c.reason}`);
2344
2587
  const text = [summary, ...notes, ...skipped].filter(Boolean).join("; ");
2345
2588
  const held = done.flatMap((x) => x.c.blocked?.length ? [{ repo: x.repo, name: x.name, blocked: x.c.blocked }] : []);
2346
- if (!done.length) return failed(text);
2347
- if (held.length) return { message: text, then: allowTracked(held) };
2348
- return skipped.length ? failed(text) : text;
2589
+ const result2 = !done.length ? failed(text) : held.length ? { message: text, then: allowTracked(held) } : skipped.length ? failed(text) : text;
2590
+ return { result: result2, skipped, notes };
2349
2591
  };
2350
2592
  const allowTracked = (held) => {
2351
2593
  const repos = [...new Set(held.map((h) => h.repo))];
@@ -2463,29 +2705,19 @@ function realOps(roots, backups) {
2463
2705
  const moved = [];
2464
2706
  const notes = [];
2465
2707
  const held = [];
2466
- for (const m of skills) {
2467
- if (importSkill(m.path).status === "exists") {
2468
- notes.push(`${m.name}: your library has a different ${m.name}; nothing moved (update your library from this copy first)`);
2469
- continue;
2470
- }
2471
- if (group) recordOrigin(m.name, `group: ${group}`);
2472
- const results = repos.map((r2) => [r2, addSkill(rootOf(r2), m.name)]);
2473
- const skipped = results.filter(([, c]) => c.action === "skipped");
2474
- if (skipped.length) {
2475
- notes.push(`${m.name}: not added to ${skipped.map(([r2, c]) => `${r2} (${c.reason})`).join(", ")}, so it still loads globally`);
2476
- continue;
2477
- }
2478
- const r = unloadGlobal(m.path, m.links);
2479
- if (!r.ok) {
2480
- notes.push(`${m.name}: ${r.reason}`);
2708
+ const repoRoots = repos.map(rootOf);
2709
+ for (const mv of moveGlobalSkills(skills, repoRoots, { group, label: (root) => repos[repoRoots.indexOf(root)] })) {
2710
+ if (!mv.moved) {
2711
+ notes.push(`${mv.name}: ${mv.reason}`);
2481
2712
  continue;
2482
2713
  }
2483
- moved.push(m.name);
2484
- for (const [repo, c] of results) {
2485
- const blind = visibilityOf(rootOf(repo), m.name).flatMap((v) => v.paths === 0 ? [v.id] : []);
2714
+ moved.push(mv.name);
2715
+ for (const [i, [root, c]] of mv.changes.entries()) {
2716
+ const repo = repos[i];
2717
+ const blind = visibilityOf(root, mv.name).flatMap((v) => v.paths === 0 ? [v.id] : []);
2486
2718
  const blocked = [...c.blocked ?? [], ...c.uncommitted ?? []];
2487
2719
  if (!blocked.length || !blind.length) continue;
2488
- held.push({ repo, name: m.name, blocked, blind, copy: copyDir(repo, m.name), shared: !c.uncommitted?.length });
2720
+ held.push({ repo, name: mv.name, blocked, blind, copy: copyDir(repo, mv.name), shared: !c.uncommitted?.length });
2489
2721
  }
2490
2722
  }
2491
2723
  const gaps = /* @__PURE__ */ new Map();
@@ -2519,33 +2751,44 @@ function realOps(roots, backups) {
2519
2751
  const r = turnOffIn(plugin, root);
2520
2752
  return r.ok ? r.message : failed(r.message);
2521
2753
  },
2522
- replacePlugin: (id, repos) => {
2523
- const skills = machineSkills().filter((s) => s.kind === "plugin" && s.origin === id && !s.broken);
2524
- const clashes = skills.filter((s) => importSkill(s.path).status === "exists").map((s) => s.name);
2754
+ replacePlugin: (p, repos) => {
2755
+ const id = p.id;
2756
+ const imported = p.skills.map((s) => ({ name: s.name, status: importSkill(s.path).status }));
2757
+ const clashes = imported.filter((r) => r.status === "exists").map((r) => r.name);
2525
2758
  if (clashes.length) return failed(`${id} kept: your library has different ${clashes.join(", ")}; update or delete ${clashes.length === 1 ? "it" : "them"} first`);
2526
- const skipped = repos.flatMap((r) => skills.flatMap((s) => {
2527
- const c = addSkill(rootOf(r), s.name);
2528
- return c.action === "skipped" ? [`${s.name} in ${r} (${c.reason})`] : [];
2529
- }));
2530
- if (skipped.length) return failed(`${id} kept: not added ${skipped.join(", ")}`);
2531
- const copied = `${skills.length} skill${skills.length === 1 ? " from " + id + " is" : "s from " + id + " are"} in your library${repos.length ? ` and in ${repos.join(", ")}` : ""}`;
2759
+ for (const r of imported) if (r.status !== "unchanged") recordOrigin(r.name, `plugin: ${id}`);
2760
+ const added = repos.length ? addMany(repos, p.skills.map((s) => s.name)) : { result: "", skipped: [], notes: [] };
2761
+ if (added.skipped.length) return failed(`${id} kept: ${added.skipped.join("; ")}`);
2762
+ const n = p.skills.length;
2763
+ const copied = `${n} skill${n === 1 ? " from " + id + " is" : "s from " + id + " are"} in your library${repos.length ? ` and in ${repos.join(", ")}` : ""}`;
2764
+ const notes = added.notes.length ? `; ${added.notes.join("; ")}` : "";
2765
+ const then = followUp(added.result);
2766
+ const done = (message) => then ? { message: message + notes, then } : message + notes;
2767
+ const ref = refOf(p);
2532
2768
  const bin = claudeBinary();
2533
- const claude = (...args) => spawnSync3(bin, ["plugin", ...args], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: 2e4 });
2534
- const synced = id.endsWith("@synced");
2535
- const off = bin && !synced && claude("uninstall", id, "--keep-data").status === 0 ? "uninstalled" : bin ? claude("disable", id) : null;
2536
- if (off === "uninstalled") {
2537
- recordRemovedPlugin(id, "user");
2538
- return `${copied}; ${id} is uninstalled (restore it from Settings \u203A Backups, or reinstall with /plugin)`;
2769
+ let uninstallFailed = "";
2770
+ if (bin && !ref.synced && !ref.settingsOnly) {
2771
+ const r = uninstallPlugin(ref);
2772
+ if (r.ok) return done(`${copied}; ${r.message}`);
2773
+ uninstallFailed = ` (uninstalling failed: ${r.message.replace(`${id}: `, "")})`;
2539
2774
  }
2540
- if (off?.status === 0)
2541
- return synced ? `${copied}; ${id} is off (it's synced from claude.ai: remove it there to delete it for good)` : `${copied}; ${id} is off: finish with /plugin uninstall ${id}`;
2542
- const why = !off ? "the claude command isn't on your PATH" : off.error ? `claude didn't answer: ${off.error.message}` : (off.stderr || off.stdout).trim().split("\n")[0] || `exit ${off.status}`;
2543
- try {
2544
- setPluginEnabled(id, false);
2545
- } catch (e) {
2546
- return failed(`${copied}; couldn't turn ${id} off (${why}; ${e.message}): turn it off with /plugin`);
2547
- }
2548
- return `${copied}; turned ${id} off in ~/.claude/settings.json (${why})${synced ? "" : `: finish with /plugin uninstall ${id}`}`;
2775
+ const off = setPluginOn(ref, false);
2776
+ if (!off.ok) return failed(`${copied}; couldn't turn ${id} off (${off.message}): turn it off with /plugin${notes}`);
2777
+ if (ref.synced) return done(`${copied}; ${id} is off (it's synced from claude.ai: remove it there to delete it for good)`);
2778
+ if (ref.settingsOnly) return done(`${copied}; ${id} is off (it was only turned on in your settings, so there's nothing to uninstall)`);
2779
+ return done(`${copied}; ${id} is off${bin ? uninstallFailed : " (the claude command isn't on your PATH)"}: finish with /plugin uninstall ${id}`);
2780
+ },
2781
+ setPlugin: (p, on) => {
2782
+ const r = setPluginOn(refOf(p), on);
2783
+ return r.ok ? r.message : failed(r.message);
2784
+ },
2785
+ uninstallPlugin: (p) => {
2786
+ const r = uninstallPlugin(refOf(p));
2787
+ return r.ok ? r.message : failed(r.message);
2788
+ },
2789
+ updatePlugin: (p) => {
2790
+ const r = updatePlugin(refOf(p));
2791
+ return r.ok ? r.message : failed(r.message);
2549
2792
  },
2550
2793
  deleteLibrary: (name) => {
2551
2794
  const from = [];
@@ -2584,7 +2827,7 @@ function realOps(roots, backups) {
2584
2827
  },
2585
2828
  copy: (text, what) => {
2586
2829
  const r = copyText(text);
2587
- return r.copied ? `Copied ${what}: paste it into Claude Code, Cursor or Codex` : `Couldn't reach the clipboard; ${what} is in ${tildify4(r.savedTo)}`;
2830
+ return r.copied ? `Copied ${what}: paste it into Claude Code, Cursor, Codex or Grok` : `Couldn't reach the clipboard; ${what} is in ${tildify4(r.savedTo)}`;
2588
2831
  },
2589
2832
  installAgentSkill: () => {
2590
2833
  const r = installAgentSkill();