skilllib 1.7.0 → 1.10.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,
@@ -44,7 +47,6 @@ import {
44
47
  readManifest,
45
48
  readSkillInfo,
46
49
  recordOrigin,
47
- recordRemovedPlugin,
48
50
  relativeTo,
49
51
  relinkDependency,
50
52
  removeAgentSkill,
@@ -57,18 +59,20 @@ import {
57
59
  setHarnesses,
58
60
  setHidden,
59
61
  setKeepGlobal,
60
- setPluginEnabled,
62
+ setPluginOn,
61
63
  skilllibHome,
62
64
  skillsLoadedIn,
63
65
  syncProject,
64
66
  tildify,
65
67
  turnOffIn,
68
+ uninstallPlugin,
66
69
  unloadGlobal,
70
+ updatePlugin,
67
71
  updateProject,
68
72
  versionHistory,
69
73
  visibilityOf,
70
74
  visibleProjects
71
- } from "./chunk-AYAN7Q2X.js";
75
+ } from "./chunk-Z2F5W3DY.js";
72
76
 
73
77
  // src/tui/index.tsx
74
78
  import { render } from "ink";
@@ -240,7 +244,7 @@ function buildReviewPrompt(skills, ctx) {
240
244
  - 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
245
  - "Global" skills load in EVERY repo for the listed agents. Project skills only load inside that repo.
242
246
  - 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.
247
+ - 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
248
  - 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
249
 
246
250
  ## My projects
@@ -543,7 +547,7 @@ function issuesOf(w, projectName, u) {
543
547
  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
548
  short: "Cursor also lists a plugin's copy",
545
549
  decision: true,
546
- fixes: [replacePluginFix(w, vendor.where)]
550
+ fixes: [pluginFixFor(w, vendor.where)]
547
551
  });
548
552
  else if (vendor)
549
553
  issues.push({
@@ -745,25 +749,117 @@ function deleteLibraryFix(w, names) {
745
749
  run: (w2) => joined(names.map((n) => w2.ops.deleteLibrary(n)))
746
750
  };
747
751
  }
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");
752
+ var plural = (n, what) => `${n} ${what}${n === 1 ? "" : "s"}`;
753
+ function pluginName(p) {
754
+ return p.id.split("@")[0];
755
+ }
756
+ function pluginSkillKey(p, skill) {
757
+ return `${pluginName(p)}:${skill}`;
758
+ }
759
+ function pluginUses(w, p, repo) {
760
+ return p.skills.reduce((n, s) => n + (repo ? w.usage[repo]?.[pluginSkillKey(p, s.name)] ?? 0 : totalUses(w, pluginSkillKey(p, s.name))), 0);
761
+ }
762
+ function pluginScope(p) {
763
+ return p.repo ? `${p.scope} ${p.repo}` : p.scope;
764
+ }
765
+ function replacePluginFix(p) {
766
+ const names = p.skills.map((s) => s.name);
767
+ const synced = p.scope === "claude.ai";
768
+ const offOnly = synced || !!p.settingsOnly;
753
769
  return {
754
- label: `Replace ${id.split("@")[0]} with library skills\u2026`,
770
+ label: `Replace ${pluginName(p)} with library skills\u2026`,
755
771
  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."
772
+ `Copies its ${plural(names.length, "skill")} into your library and adds them to the repos you tick (or none),`,
773
+ 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).`,
774
+ p.parts.length ? `It also brings ${p.parts.join(", ")}: those ${offOnly ? "stop" : "go"} too.` : "",
775
+ offOnly ? "Turn it back on from Plugins." : "Settings \u203A Backups reinstalls it."
760
776
  ].filter(Boolean).join(" "),
761
777
  run: () => "",
762
778
  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)
779
+ preticked: (w) => w.projects.filter((x) => pluginUses(w, p, x.name) > 0).map((x) => x.name),
780
+ pickRepos: (w, repos) => w.ops.replacePlugin(p, repos)
781
+ };
782
+ }
783
+ function pluginFixFor(w, id) {
784
+ const ofId = w.plugins.filter((p2) => p2.agent === "claude-code" && p2.id === id);
785
+ const p = ofId.find((x) => !x.repo) ?? ofId[0];
786
+ if (p) return replacePluginFix(p);
787
+ 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` };
788
+ }
789
+ function pluginSwitchFix(p, on) {
790
+ const where = p.repo ? ` in ${p.repo} (${p.scope} install)` : p.scope === "claude.ai" ? " on this machine" : " in every repo";
791
+ return {
792
+ label: on ? "Turn it on" : "Turn it off",
793
+ 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.`,
794
+ run: (w) => w.ops.setPlugin(p, on)
765
795
  };
766
796
  }
797
+ function uninstallPluginFix(p) {
798
+ return {
799
+ label: "Uninstall",
800
+ 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.`,
801
+ run: (w) => w.ops.uninstallPlugin(p)
802
+ };
803
+ }
804
+ function updatePluginFix(p) {
805
+ return {
806
+ label: p.update === "newer" ? "Update it" : `Update to ${p.update}`,
807
+ 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.`,
808
+ run: (w) => w.ops.updatePlugin(p)
809
+ };
810
+ }
811
+ function pluginIssues(w, p) {
812
+ const library = new Set(w.library.map((l) => l.name));
813
+ const global = new Set(w.machine.filter((m) => m.source === "global" && !m.broken).map((m) => m.name));
814
+ if (p.agent === "cursor") {
815
+ const dupes = p.skills.filter((s) => library.has(s.name) || global.has(s.name));
816
+ return dupes.length ? [
817
+ {
818
+ id: `plugin-dupes:${p.key}`,
819
+ severity: "warning",
820
+ title: `Repeats your ${dupes.map((s) => s.name).join(", ")}: Cursor lists both while it's on`,
821
+ short: `Repeats ${dupes.length} of yours`,
822
+ decision: true,
823
+ 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` }]
824
+ }
825
+ ] : [];
826
+ }
827
+ const issues = [];
828
+ if (p.on) {
829
+ const repo = p.repo ? w.projects.find((x) => x.name === p.repo) : void 0;
830
+ const here = repo ? new Set(repo.skills.map((s) => s.name)) : void 0;
831
+ const yours = (n) => global.has(n) || (p.repo ? !!here?.has(n) : library.has(n));
832
+ const dupes = p.skills.filter((s) => yours(s.name));
833
+ if (dupes.length)
834
+ issues.push({
835
+ id: `plugin-dupes:${p.key}`,
836
+ severity: "warning",
837
+ title: `Repeats your ${dupes.map((s) => s.name).join(", ")}`,
838
+ short: `Repeats ${dupes.length} of yours`,
839
+ decision: true,
840
+ fixes: [replacePluginFix(p), pluginSwitchFix(p, false)]
841
+ });
842
+ if (w.days && p.skills.length && pluginUses(w, p) === 0)
843
+ issues.push({
844
+ id: `plugin-unused:${p.key}`,
845
+ severity: "hint",
846
+ title: `Its skills weren't used in 30 days (Claude Code)${p.parts.length ? `; it also brings ${p.parts.join(", ")}` : ""}`,
847
+ short: "Unused 30 days",
848
+ decision: true,
849
+ fixes: [pluginSwitchFix(p, false), ...p.scope === "claude.ai" || p.settingsOnly ? [] : [uninstallPluginFix(p)]]
850
+ });
851
+ }
852
+ if (p.update)
853
+ issues.push({
854
+ id: `plugin-update:${p.key}`,
855
+ severity: "hint",
856
+ title: p.update === "newer" ? "Its marketplace has a newer commit" : `Update available: ${p.version ?? "?"} \u2192 ${p.update}`,
857
+ short: p.update === "newer" ? "Update available" : `Update to ${p.update}`,
858
+ decision: true,
859
+ fixes: [updatePluginFix(p)]
860
+ });
861
+ return issues;
862
+ }
767
863
  function originGroup(origin) {
768
864
  if (!origin) return null;
769
865
  const plugin = origin.match(/^plugin: (.+)$/);
@@ -788,6 +884,7 @@ function groupCandidates(w, u) {
788
884
  if (m?.source === "plugin") return [{ key: `plugin:${m.where}`, label: `\u29C9 ${m.where.split("@")[0]} plugin` }];
789
885
  if (m?.source === "claude.ai") return [{ key: "claude.ai", label: "claude.ai skills" }];
790
886
  if (m?.source === "cursor") return [{ key: "cursor", label: "Cursor built-ins" }];
887
+ if (m?.source === "grok") return [{ key: "grok", label: "Grok bundled" }];
791
888
  if (m?.source === "system") return [{ key: `system:${m.where}`, label: m.where }];
792
889
  if (m?.source === "skilllib") return [];
793
890
  if (m) {
@@ -918,6 +1015,7 @@ var PLACES = [
918
1015
  ["projects", "Projects"],
919
1016
  ["library", "Your skills"],
920
1017
  ["global", "Global"],
1018
+ ["plugins", "Plugins"],
921
1019
  ["health", "Health"],
922
1020
  ["settings", "Settings"]
923
1021
  ];
@@ -996,7 +1094,7 @@ function TitleRow({ cells, width }) {
996
1094
  }
997
1095
  var fixOption = (fix) => ({ label: fix.label, fix });
998
1096
  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 };
1097
+ 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
1098
  function byName(copies) {
1001
1099
  const groups = /* @__PURE__ */ new Map();
1002
1100
  for (const c of copies) groups.set(c.name, [...groups.get(c.name) ?? [], c]);
@@ -1139,11 +1237,13 @@ function overview(w, p) {
1139
1237
  const project2 = project(w, p);
1140
1238
  const info = w.ops.repoInfo(p);
1141
1239
  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];
1240
+ const plugins = [...new Set(machineIn(w, project2).filter((m) => m.source === "plugin").map((m) => m.where.split("@")[0]))];
1142
1241
  return [
1143
1242
  ["Folder", [[tildify3(project2.path), color.text]]],
1144
1243
  ["Remote", [[info.remote ?? "none", info.remote ? color.blue : color.faint]]],
1145
1244
  ["Branch", [[info.branch ?? "\u2013", color.text], [info.dirty ? ` ${info.dirty} uncommitted` : "", color.yellow]]],
1146
- ["Manifest", [manifest]]
1245
+ ["Manifest", [manifest]],
1246
+ ["Plugins", [[plugins.length ? plugins.join(", ") : "none", plugins.length ? color.magenta : color.faint]]]
1147
1247
  ];
1148
1248
  }
1149
1249
  function InfoGrid({ rows, columns, width }) {
@@ -1185,13 +1285,21 @@ function globalItems(w, nameW, ui) {
1185
1285
  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
1286
  return byName(copies).map((g) => skillItem(w, g, mergeIssues(g.map((c) => machineIssues(w, c.machine))), nameW, globalActions(w, g[0], ui), null));
1187
1287
  }
1288
+ var AT_SOURCE = {
1289
+ global: null,
1290
+ plugin: null,
1291
+ skilllib: null,
1292
+ "claude.ai": "Turn it off in claude.ai \u203A Settings",
1293
+ cursor: "Cursor manages it",
1294
+ grok: "Ships with Grok; a same-named skill of yours overrides it"
1295
+ };
1188
1296
  function globalActions(w, u, ui) {
1189
1297
  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";
1298
+ const atSource = m.source === "system" ? `An admin manages ${m.where}` : AT_SOURCE[m.source];
1191
1299
  return [
1192
1300
  ...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`) }],
1301
+ ...m.source === "plugin" ? [{ label: `Open ${m.where.split("@")[0]} in Plugins`, action: () => ui.openInPlugins(m.where) }] : [],
1302
+ ...atSource ? [{ label: atSource, action: () => ui.notify(`${atSource}; skilllib picks up the change next time`) }] : [],
1195
1303
  ...m.source === "global" && !m.broken ? [{ label: "Edit SKILL.md", action: () => ui.edit(`${m.path}/SKILL.md`) }] : [],
1196
1304
  reviewOption(w, m.name, ui)
1197
1305
  ];
@@ -1283,6 +1391,66 @@ function agentSkillItems(w, nameW) {
1283
1391
  }
1284
1392
  ];
1285
1393
  }
1394
+ var pluginW = (nameW) => Math.min(nameW, 17);
1395
+ var STATE = { on: ["on", color.green], off: ["off", color.faint], unknown: ["?", color.faint] };
1396
+ function pluginItems(w, skillW, ui, issuesOf2) {
1397
+ const nameW = pluginW(skillW);
1398
+ return w.plugins.map((p) => {
1399
+ const issues = issuesOf2.get(p.key) ?? [];
1400
+ const top = worst(issues);
1401
+ const cursor = p.agent === "cursor";
1402
+ const uses = cursor ? 0 : pluginUses(w, p);
1403
+ const [state, stateColor] = STATE[p.on === null ? "unknown" : p.on ? "on" : "off"];
1404
+ const n = p.skills.length;
1405
+ const name = pluginName(p);
1406
+ 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`) }] : [
1407
+ fixOption(pluginSwitchFix(p, !p.on)),
1408
+ ...p.update ? [fixOption(updatePluginFix(p))] : [],
1409
+ ...n ? [fixOption(replacePluginFix(p))] : [],
1410
+ ...p.scope === "claude.ai" || p.settingsOnly ? [] : [fixOption(uninstallPluginFix(p))]
1411
+ ];
1412
+ return {
1413
+ key: p.key,
1414
+ name: p.id,
1415
+ plugin: p,
1416
+ issues,
1417
+ uses,
1418
+ cells: [
1419
+ { text: top ? SEV[top.severity].icon : " ", width: 2, color: top ? SEV[top.severity].color : void 0 },
1420
+ { text: clip(name, nameW), width: nameW, color: p.on === false ? color.muted : void 0 },
1421
+ { text: harness(p.agent).icon, width: 3, color: harness(p.agent).color },
1422
+ { text: clip(cursor ? "\u2013" : pluginScope(p), 19), width: 19, color: p.repo ? color.blue : color.muted },
1423
+ { text: clip(`${n} skill${n === 1 ? "" : "s"}${p.parts.length ? ` +${p.parts.length}` : ""}`, 13), width: 13, color: color.muted },
1424
+ { text: state.padEnd(4), width: 4, color: stateColor },
1425
+ { text: cursor ? " \u2013 " : usesText(uses), width: 6, color: color.muted },
1426
+ { text: " " + (top?.short ?? ""), grow: true, color: top ? SEV[top.severity].color : color.faint }
1427
+ ],
1428
+ detail: (width) => {
1429
+ const b = builder(p.id);
1430
+ for (const l of wrap(p.description || "(no description)", width - 4, 2)) b.line([l, color.muted]);
1431
+ b.line();
1432
+ b.issues(issues);
1433
+ b.actions([...actions, { label: "Open its folder", action: () => ui.notify(said2(w.ops.openFolder(p.path))) }, {
1434
+ label: "Review prompt",
1435
+ action: () => ui.copy(
1436
+ 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?`),
1437
+ `a review prompt for ${name}`
1438
+ )
1439
+ }], width);
1440
+ b.line();
1441
+ const label = (t) => [t.padEnd(12), color.muted];
1442
+ const uses2 = (n2) => n2 ? `${n2} use${n2 === 1 ? "" : "s"}` : "unused";
1443
+ const skills = p.skills.map((s) => cursor || !w.days ? s.name : `${s.name} (${uses2(totalUses(w, pluginSkillKey(p, s.name)))})`);
1444
+ wrap(skills.join(" \xB7 ") || "none", width - 16, 4).forEach((l, i) => b.line(label(i ? "" : "Skills"), [l, color.text]));
1445
+ b.line(label("Also brings"), [p.parts.join(", ") || "nothing else", p.parts.length ? color.text : color.faint]);
1446
+ 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`;
1447
+ 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]);
1448
+ b.line(label("Folder"), [tildify3(p.path), color.faint]);
1449
+ return b.d;
1450
+ }
1451
+ };
1452
+ });
1453
+ }
1286
1454
  function addCursor(rows, cursor) {
1287
1455
  const i = Math.min(cursor, rows.length - 1);
1288
1456
  return rows[i] && !rows[i].header ? i : Math.max(0, rows.findIndex((r) => !r.header));
@@ -1324,10 +1492,12 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1324
1492
  const ui = {
1325
1493
  notify: setToast,
1326
1494
  openInGlobal,
1495
+ openInPlugins,
1327
1496
  edit,
1328
1497
  copy: (text, what) => setToast(said2(world.ops.copy(text, what))),
1329
1498
  menu: (title, options) => setModal({ kind: "menu", title, options, cursor: 0 })
1330
1499
  };
1500
+ const pluginIssues2 = new Map(world.plugins.map((p) => [p.key, pluginIssues(world, p)]));
1331
1501
  const flagged = [
1332
1502
  ...world.projects.flatMap((p) => projectIssues(world, p.name).map((x) => ({ key: `${p.name}:${x.skill.name}`, issue: x.issue }))),
1333
1503
  ...world.machine.flatMap((m) => machineIssues(world, m).map((issue) => ({ key: `Global:${m.name}`, issue }))),
@@ -1336,7 +1506,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1336
1506
  ];
1337
1507
  const healthCount = new Set(flagged.map((f) => f.key)).size;
1338
1508
  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();
1509
+ 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
1510
  const inChip = (i, c) => c === "all" || (c === "issues" ? i.issues.length > 0 : i.group === c);
1341
1511
  let items = all.filter((i) => i.header || (!query || matches(i.name, query)) && inChip(i, chip));
1342
1512
  const viewKey = [place, open, chip, query, used ? "usage" : ""].join("|");
@@ -1485,8 +1655,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1485
1655
  ] : [],
1486
1656
  ...libs.length ? [fixOption(deleteLibraryFix(world, libs.map((l) => l.name)))] : [],
1487
1657
  ...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") }] : [],
1658
+ ...[...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
1659
  { label: "Review prompt", action: () => ui.copy(reviewPrompt(world, names, `Review these related skills (${label}).`), `a review prompt for ${label}`) }
1491
1660
  ];
1492
1661
  }
@@ -1579,6 +1748,10 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1579
1748
  goPlace("global");
1580
1749
  setQuery(name);
1581
1750
  }
1751
+ function openInPlugins(id) {
1752
+ goPlace("plugins");
1753
+ setQuery(id);
1754
+ }
1582
1755
  function settingsItems() {
1583
1756
  const header = (key, text) => ({ key, name: "", header: true, issues: [], uses: 0, cells: [{ text }] });
1584
1757
  const row = (key, text, sub, onEnter, c) => ({
@@ -1812,6 +1985,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1812
1985
  const globalFlagged = world.machine.map((m) => [m.name, machineIssues(world, m)]).filter(([, issues]) => issues.length);
1813
1986
  const globalIssues = new Set(globalFlagged.map(([name]) => name)).size;
1814
1987
  const globalProblem = globalFlagged.some(([, issues]) => issues.some((x) => x.severity === "problem"));
1988
+ const pluginsWorst = worst([...pluginIssues2.values()].flat().filter((i) => i.severity !== "hint"));
1815
1989
  const badge = (p) => {
1816
1990
  const found = projectIssues(world, p);
1817
1991
  const top = worst(found.map((x) => x.issue));
@@ -1833,6 +2007,9 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1833
2007
  sideRow("global", "\u25C8 Global", place === "global", () => goPlace("global"), [
1834
2008
  { text: String(new Set(world.machine.map((m) => m.name)).size), width: 4, color: globalProblem ? color.red : globalIssues ? color.yellow : color.muted }
1835
2009
  ]),
2010
+ sideRow("plugins", "\u29C9 Plugins", place === "plugins", () => goPlace("plugins"), [
2011
+ { text: String(world.plugins.length), width: 4, color: pluginsWorst ? SEV[pluginsWorst.severity].color : color.muted }
2012
+ ]),
1836
2013
  sideRow("health", "\u2713 Health", place === "health", () => goPlace("health"), [
1837
2014
  { text: String(healthCount), width: 4, color: healthWorst ? SEV[healthWorst.severity].color : color.green }
1838
2015
  ]),
@@ -1870,17 +2047,17 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1870
2047
  }),
1871
2048
  /* @__PURE__ */ jsx2(Text2, { color: color.accent, children: query ? ` \u2315 ${query}\u258F` : "" })
1872
2049
  ] }) });
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;
2050
+ 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
2051
  const showChips = chips.length > 0;
1875
2052
  const listHeader = /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
1876
2053
  showChips ? chipRow : null,
1877
2054
  columnTitles ? /* @__PURE__ */ jsx2(TitleRow, { cells: columnTitles, width: columns }) : null
1878
2055
  ] });
1879
- const overviewH = dashboard && !sidebar ? 4 : 0;
2056
+ const overviewH = dashboard && !sidebar ? Math.ceil(overview(world, open).length / 2) + 2 : 0;
1880
2057
  const listH = split ? Math.floor((bodyH - overviewH) / 2) : bodyH - overviewH;
1881
2058
  const detailH = split ? bodyH - overviewH - listH : bodyH;
1882
2059
  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";
2060
+ const listTitle = dashboard ? "Skills" : place === "projects" ? "Projects" : place === "library" ? "Your skills" : place === "global" ? "Loads everywhere" : place === "plugins" ? "Plugins" : place === "health" ? "To fix" : "Settings";
1884
2061
  const list = /* @__PURE__ */ jsx2(
1885
2062
  ListPanel,
1886
2063
  {
@@ -1944,6 +2121,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1944
2121
  else if (modal?.kind === "repos") {
1945
2122
  const repos = (modal.only ?? world.projects.map((p) => p.name)).filter((r) => !modal.query || matches(r, modal.query));
1946
2123
  const name = current?.name ?? "";
2124
+ const usedHere = (r) => current?.plugin ? pluginUses(world, current.plugin, r) : world.usage[r]?.[name] ?? 0;
1947
2125
  const pickerLines = wrap(modal.fix.preview, columns - 6, 4);
1948
2126
  body = /* @__PURE__ */ jsx2(
1949
2127
  ListPanel,
@@ -1964,7 +2142,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1964
2142
  cells: [
1965
2143
  { text: modal.picked.has(r) ? "[x] " : "[ ] ", width: 4, color: modal.picked.has(r) ? color.green : color.faint },
1966
2144
  { text: r, width: 20 },
1967
- { text: world.usage[r]?.[name] ? `used ${world.usage[r][name]}\xD7 here` : "", grow: true, color: color.faint }
2145
+ { text: usedHere(r) ? `used ${usedHere(r)}\xD7 here` : "", grow: true, color: color.faint }
1968
2146
  ]
1969
2147
  }))
1970
2148
  }
@@ -2100,7 +2278,7 @@ function Help({ width, height }) {
2100
2278
  }
2101
2279
 
2102
2280
  // src/tui/load.ts
2103
- import { execFileSync, spawn, spawnSync as spawnSync3 } from "child_process";
2281
+ import { execFileSync, spawn } from "child_process";
2104
2282
  import { existsSync as existsSync3, lstatSync, realpathSync } from "fs";
2105
2283
  import { basename, dirname, join as join2 } from "path";
2106
2284
  function tildify4(p) {
@@ -2155,20 +2333,6 @@ function localSkill(root, s, git2) {
2155
2333
  git: state
2156
2334
  };
2157
2335
  }
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
2336
  function installedAt(path) {
2173
2337
  try {
2174
2338
  const st = lstatSync(path);
@@ -2182,7 +2346,7 @@ function machineSkill(s, kept) {
2182
2346
  const global = s.kind === "global" || s.kind === "skills.sh";
2183
2347
  return {
2184
2348
  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",
2349
+ 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
2350
  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
2351
  ...s.kind === "skills.sh" ? { origin: `npx skills: ${s.origin}` } : {},
2188
2352
  path: s.path,
@@ -2190,10 +2354,31 @@ function machineSkill(s, kept) {
2190
2354
  agents: s.harnesses,
2191
2355
  ...global && kept.has(s.name) ? { kept: true } : {},
2192
2356
  ...s.broken ? { broken: true } : {},
2193
- ...global && s.kind !== "skills.sh" ? { installed: installedAt(s.path) } : {},
2194
- ...s.kind === "plugin" ? { pluginParts: pluginParts(dirname(dirname(s.path))) } : {}
2357
+ ...global && s.kind !== "skills.sh" ? { installed: installedAt(s.path) } : {}
2358
+ };
2359
+ }
2360
+ function pluginOf(p, repoOf) {
2361
+ const scope = p.agent === "cursor" ? "cursor" : p.synced ? "claude.ai" : p.scope ?? "user";
2362
+ const repo = p.projectPath ? repoOf(p.projectPath) ?? basename(p.projectPath) : void 0;
2363
+ return {
2364
+ key: [p.agent, p.id, scope, p.projectPath ?? ""].join("|"),
2365
+ id: p.id,
2366
+ agent: p.agent,
2367
+ scope,
2368
+ ...repo ? { repo, projectPath: p.projectPath } : {},
2369
+ on: p.on,
2370
+ ...p.version ? { version: p.version } : {},
2371
+ ...p.update ? { update: p.update } : {},
2372
+ ...p.settingsOnly ? { settingsOnly: true } : {},
2373
+ description: p.description,
2374
+ path: p.root,
2375
+ skills: p.skills.map((path) => ({ name: basename(path), path })),
2376
+ parts: p.extras
2195
2377
  };
2196
2378
  }
2379
+ function refOf(p) {
2380
+ 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 } : {} };
2381
+ }
2197
2382
  function dupesOf(r, root, git2 = null) {
2198
2383
  const out = /* @__PURE__ */ new Map();
2199
2384
  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 +2388,7 @@ function dupesOf(r, root, git2 = null) {
2203
2388
  }
2204
2389
  function loadWorld() {
2205
2390
  forgetLatest();
2391
+ forgetClaudeBinary();
2206
2392
  const agents = enabledHarnesses();
2207
2393
  const here = projectHere();
2208
2394
  const real = (p) => {
@@ -2223,7 +2409,8 @@ function loadWorld() {
2223
2409
  return { name: s.name, latest: latestVersion(s.name)?.version ?? 1, origin: origins[s.name] };
2224
2410
  });
2225
2411
  const kept = keptGlobal();
2226
- const cursorPlugin = new Map(agents.includes("cursor") ? cursorPluginSkills().map((s) => [s.name, s.plugin]) : []);
2412
+ const cursorList = agents.includes("cursor") ? cursorPlugins() : [];
2413
+ const cursorPlugin = new Map(cursorPluginSkills(cursorList).map((s) => [s.name, s.plugin]));
2227
2414
  const globalDupes = dupesOf(planGlobalTidy({ enabled: agents }), null);
2228
2415
  const sourced = machineSkills(agents);
2229
2416
  const machine = sourced.map((s) => {
@@ -2234,7 +2421,7 @@ function loadWorld() {
2234
2421
  const plugin = cursorPlugin.get(m.name);
2235
2422
  return { ...m, ...dupes ? { dupes } : {}, ...plugin ? { cursorPlugin: plugin } : {} };
2236
2423
  });
2237
- const plugins = (root) => {
2424
+ const loadedHere = (root) => {
2238
2425
  const loaded = skillsLoadedIn(root, sourced, agents);
2239
2426
  if (loaded === sourced) return {};
2240
2427
  for (const s of loaded) descriptions[s.name] ??= s.description;
@@ -2255,15 +2442,18 @@ function loadWorld() {
2255
2442
  const plugin = cursorPlugin.get(s.name);
2256
2443
  return { ...localSkill(root, s, git2), ...d ? { dupes: d } : {}, ...plugin ? { cursorPlugin: plugin } : {} };
2257
2444
  }),
2258
- ...plugins(root)
2445
+ ...loadedHere(root)
2259
2446
  };
2260
2447
  });
2261
2448
  const backups = [...listBackups(), ...pluginBackups()].sort((a, b) => b.movedAt.localeCompare(a.movedAt));
2449
+ const repoOf = (path) => projects.find((p) => real(p.path) === real(path))?.name;
2450
+ const plugins = [...installedPlugins(), ...cursorList].map((p) => pluginOf(p, repoOf));
2262
2451
  return {
2263
2452
  agents,
2264
2453
  cwd: cwd ? names.get(cwd) : null,
2265
2454
  projects,
2266
2455
  machine,
2456
+ plugins,
2267
2457
  library,
2268
2458
  usage: {},
2269
2459
  descriptions,
@@ -2325,6 +2515,9 @@ function realOps(roots, backups) {
2325
2515
  };
2326
2516
  const addTo = (repos, names) => {
2327
2517
  if (repos.length === 1 && names.length === 1) return withLinks(addSkill(rootOf(repos[0]), names[0]), repos[0], names[0]);
2518
+ return addMany(repos, names).result;
2519
+ };
2520
+ const addMany = (repos, names) => {
2328
2521
  const changes = repos.flatMap((repo) => names.map((name) => ({ repo, name, c: addSkill(rootOf(repo), name) })));
2329
2522
  const done = changes.filter((x) => x.c.action !== "skipped");
2330
2523
  const added = [...new Set(done.map((x) => x.name))];
@@ -2343,9 +2536,8 @@ function realOps(roots, backups) {
2343
2536
  const skipped = changes.filter((x) => x.c.action === "skipped").map((x) => `${x.name} not added to ${x.repo}: ${x.c.reason}`);
2344
2537
  const text = [summary, ...notes, ...skipped].filter(Boolean).join("; ");
2345
2538
  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;
2539
+ const result2 = !done.length ? failed(text) : held.length ? { message: text, then: allowTracked(held) } : skipped.length ? failed(text) : text;
2540
+ return { result: result2, skipped, notes };
2349
2541
  };
2350
2542
  const allowTracked = (held) => {
2351
2543
  const repos = [...new Set(held.map((h) => h.repo))];
@@ -2519,33 +2711,44 @@ function realOps(roots, backups) {
2519
2711
  const r = turnOffIn(plugin, root);
2520
2712
  return r.ok ? r.message : failed(r.message);
2521
2713
  },
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);
2714
+ replacePlugin: (p, repos) => {
2715
+ const id = p.id;
2716
+ const imported = p.skills.map((s) => ({ name: s.name, status: importSkill(s.path).status }));
2717
+ const clashes = imported.filter((r) => r.status === "exists").map((r) => r.name);
2525
2718
  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(", ")}` : ""}`;
2719
+ for (const r of imported) if (r.status !== "unchanged") recordOrigin(r.name, `plugin: ${id}`);
2720
+ const added = repos.length ? addMany(repos, p.skills.map((s) => s.name)) : { result: "", skipped: [], notes: [] };
2721
+ if (added.skipped.length) return failed(`${id} kept: ${added.skipped.join("; ")}`);
2722
+ const n = p.skills.length;
2723
+ const copied = `${n} skill${n === 1 ? " from " + id + " is" : "s from " + id + " are"} in your library${repos.length ? ` and in ${repos.join(", ")}` : ""}`;
2724
+ const notes = added.notes.length ? `; ${added.notes.join("; ")}` : "";
2725
+ const then = followUp(added.result);
2726
+ const done = (message) => then ? { message: message + notes, then } : message + notes;
2727
+ const ref = refOf(p);
2532
2728
  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)`;
2539
- }
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`);
2729
+ let uninstallFailed = "";
2730
+ if (bin && !ref.synced && !ref.settingsOnly) {
2731
+ const r = uninstallPlugin(ref);
2732
+ if (r.ok) return done(`${copied}; ${r.message}`);
2733
+ uninstallFailed = ` (uninstalling failed: ${r.message.replace(`${id}: `, "")})`;
2547
2734
  }
2548
- return `${copied}; turned ${id} off in ~/.claude/settings.json (${why})${synced ? "" : `: finish with /plugin uninstall ${id}`}`;
2735
+ const off = setPluginOn(ref, false);
2736
+ if (!off.ok) return failed(`${copied}; couldn't turn ${id} off (${off.message}): turn it off with /plugin${notes}`);
2737
+ if (ref.synced) return done(`${copied}; ${id} is off (it's synced from claude.ai: remove it there to delete it for good)`);
2738
+ if (ref.settingsOnly) return done(`${copied}; ${id} is off (it was only turned on in your settings, so there's nothing to uninstall)`);
2739
+ return done(`${copied}; ${id} is off${bin ? uninstallFailed : " (the claude command isn't on your PATH)"}: finish with /plugin uninstall ${id}`);
2740
+ },
2741
+ setPlugin: (p, on) => {
2742
+ const r = setPluginOn(refOf(p), on);
2743
+ return r.ok ? r.message : failed(r.message);
2744
+ },
2745
+ uninstallPlugin: (p) => {
2746
+ const r = uninstallPlugin(refOf(p));
2747
+ return r.ok ? r.message : failed(r.message);
2748
+ },
2749
+ updatePlugin: (p) => {
2750
+ const r = updatePlugin(refOf(p));
2751
+ return r.ok ? r.message : failed(r.message);
2549
2752
  },
2550
2753
  deleteLibrary: (name) => {
2551
2754
  const from = [];
@@ -2584,7 +2787,7 @@ function realOps(roots, backups) {
2584
2787
  },
2585
2788
  copy: (text, what) => {
2586
2789
  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)}`;
2790
+ 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
2791
  },
2589
2792
  installAgentSkill: () => {
2590
2793
  const r = installAgentSkill();