skilllib 1.6.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.
@@ -1,7 +1,10 @@
1
1
  import {
2
2
  HARNESSES,
3
+ PROJECT_SKILLS_DIR,
3
4
  addRoot,
4
5
  addSkill,
6
+ addedAt,
7
+ agentSkillState,
5
8
  allowTrackedLinks,
6
9
  applyTidy,
7
10
  claudeBinary,
@@ -9,19 +12,21 @@ import {
9
12
  copyLabel,
10
13
  createSkill,
11
14
  cursorPluginSkills,
15
+ cursorPlugins,
12
16
  deleteGlobal,
13
17
  deleteLibrarySkill,
14
18
  describeStep,
15
19
  discoverProjects,
16
20
  enabledHarnesses,
17
- findProjectRoot,
21
+ forgetClaudeBinary,
18
22
  forgetLatest,
19
23
  gitInfo,
20
24
  gitVisibleSteps,
21
25
  harness,
22
26
  harnessesChosen,
23
27
  importSkill,
24
- isProjectCandidate,
28
+ installAgentSkill,
29
+ installedPlugins,
25
30
  keptGlobal,
26
31
  latestVersion,
27
32
  libraryOrigins,
@@ -34,15 +39,17 @@ import {
34
39
  planGlobalTidy,
35
40
  planProjectTidy,
36
41
  pluginBackups,
42
+ projectHere,
37
43
  projectOfFactory,
38
44
  projectStatus,
39
45
  projectsUsing,
40
46
  readConfig,
47
+ readManifest,
41
48
  readSkillInfo,
42
49
  recordOrigin,
43
- recordRemovedPlugin,
44
50
  relativeTo,
45
51
  relinkDependency,
52
+ removeAgentSkill,
46
53
  removeRoot,
47
54
  removeSkill,
48
55
  removeUntracked,
@@ -52,17 +59,20 @@ import {
52
59
  setHarnesses,
53
60
  setHidden,
54
61
  setKeepGlobal,
55
- setPluginEnabled,
62
+ setPluginOn,
56
63
  skilllibHome,
57
64
  skillsLoadedIn,
58
65
  syncProject,
66
+ tildify,
59
67
  turnOffIn,
68
+ uninstallPlugin,
60
69
  unloadGlobal,
70
+ updatePlugin,
61
71
  updateProject,
62
- userHome,
63
72
  versionHistory,
73
+ visibilityOf,
64
74
  visibleProjects
65
- } from "./chunk-PHRUPZGX.js";
75
+ } from "./chunk-Z2F5W3DY.js";
66
76
 
67
77
  // src/tui/index.tsx
68
78
  import { render } from "ink";
@@ -188,10 +198,7 @@ import { spawnSync } from "child_process";
188
198
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
189
199
  import { join } from "path";
190
200
  var EXCERPT_CHARS = 1200;
191
- function tildify(p) {
192
- const home = userHome();
193
- return p.startsWith(home) ? "~" + p.slice(home.length) : p;
194
- }
201
+ var tildify2 = (p) => tildify(p);
195
202
  function skillExcerpt(dir, chars) {
196
203
  const file = join(dir, "SKILL.md");
197
204
  if (!existsSync(file)) return { text: "(SKILL.md not found)", files: 0, size: 0 };
@@ -215,7 +222,7 @@ function buildReviewPrompt(skills, ctx) {
215
222
  `- Description: ${s.description || "(none)"}`,
216
223
  `- Source: ${s.source}`,
217
224
  `- Installed via: ${s.installedHow}`,
218
- `- Location: ${tildify(s.path)}${s.links.length ? ` (also linked from ${s.links.map(tildify).join(", ")})` : ""}`,
225
+ `- Location: ${tildify2(s.path)}${s.links.length ? ` (also linked from ${s.links.map(tildify2).join(", ")})` : ""}`,
219
226
  `- Loaded by: ${s.loadedBy}`,
220
227
  ...s.vendor ? [`- Vendor-managed: ${s.vendor}`] : [],
221
228
  ...s.keptGlobal ? ["- I marked this as global on purpose: recommend changing that only for a strong reason (unused, duplicated, low quality)"] : [],
@@ -237,11 +244,11 @@ function buildReviewPrompt(skills, ctx) {
237
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.
238
245
  - "Global" skills load in EVERY repo for the listed agents. Project skills only load inside that repo.
239
246
  - I use these agents: ${ctx.harnesses.join(", ") || "(none configured)"}.
240
- - 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.
241
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.
242
249
 
243
250
  ## My projects
244
- ${ctx.projects.map((p) => `- ${p.name} \u2014 ${tildify(p.path)}`).join("\n") || "(none)"}
251
+ ${ctx.projects.map((p) => `- ${p.name} \u2014 ${tildify2(p.path)}`).join("\n") || "(none)"}
245
252
 
246
253
  You can open the paths above (skills and projects) to check what a skill does and what each project uses (package.json, README, tech stack).
247
254
 
@@ -292,9 +299,17 @@ function matchScore(text, query) {
292
299
  }
293
300
 
294
301
  // src/tui/world.ts
302
+ var failed = (message) => ({ message, failed: true });
303
+ var isFailure = (r) => typeof r !== "string" && "failed" in r;
304
+ var followUp = (r) => typeof r !== "string" && "then" in r ? r.then : void 0;
305
+ var said = (r) => typeof r === "string" ? r : r.message;
306
+ var joined = (results) => {
307
+ const message = results.map(said).join(" \xB7 ");
308
+ return results.some(isFailure) ? failed(message) : message;
309
+ };
295
310
  var SEVERITY_RANK = { problem: 0, warning: 1, hint: 2 };
296
311
  function filterOf(source) {
297
- return source === "lib" || source === "repo" || source === "untracked" ? "local" : source === "global" ? "global" : source === "plugin" ? "plugins" : "vendor";
312
+ return source === "lib" || source === "repo" || source === "untracked" ? "local" : source === "global" || source === "skilllib" ? "global" : source === "plugin" ? "plugins" : "vendor";
298
313
  }
299
314
  function project(w, name) {
300
315
  return w.projects.find((p) => p.name === name);
@@ -325,6 +340,10 @@ function lib(w, name) {
325
340
  return w.library.find((l) => l.name === name);
326
341
  }
327
342
  var agentNames = (ids) => ids.map((a) => harness(a).name).join(", ");
343
+ function usedElsewhere(w, except, name) {
344
+ const repos = w.projects.filter((p) => p.name !== except && (w.usage[p.name]?.[name] ?? 0) > 0 && !p.skills.some((s) => s.name === name)).map((p) => p.name);
345
+ return repos.length ? `, and ${repos.join(", ")} used it in the last 30 days` : "";
346
+ }
328
347
  function dupeIssues(repo, name, d) {
329
348
  const g = repo === null ? "-g" : "";
330
349
  if (d.differ)
@@ -381,7 +400,7 @@ function globalFixes(m) {
381
400
  label: "Move it to the repos that need it\u2026",
382
401
  preview: `Pick repos. ${m.name} goes into your library and those repos, and stops loading globally (original backed up).`,
383
402
  run: () => "",
384
- pickRepos: (w, repos) => w.ops.moveGlobal(m, repos)
403
+ pickRepos: (w, repos) => w.ops.moveGlobal([m], repos)
385
404
  },
386
405
  m.kept ? { label: "Stop marking it as global on purpose", preview: `skilllib warns about ${m.name} again, like any global skill.`, run: (w) => w.ops.keepGlobal(m.name, false) } : { label: "Keep it global on purpose", preview: `Mark ${m.name} as global on purpose. skilllib stops warning about it.`, run: (w) => w.ops.keepGlobal(m.name, true) },
387
406
  { label: "Delete it", preview: `Remove ${m.path}. A backup is kept in Settings \u203A Backups.`, run: (w) => w.ops.deleteGlobal(m) }
@@ -389,9 +408,10 @@ function globalFixes(m) {
389
408
  }
390
409
  function machineActions(m) {
391
410
  if (m.broken) return [];
392
- return m.source === "global" ? globalFixes(m) : [];
411
+ return m.source === "global" ? globalFixes(m) : m.source === "skilllib" ? [removeAgentSkillFix()] : [];
393
412
  }
394
413
  function machineIssues(w, m) {
414
+ if (m.source === "skilllib") return [agentSkillIssue(w)].flatMap((i) => i ? [i] : []);
395
415
  if (m.broken)
396
416
  return [
397
417
  {
@@ -411,11 +431,12 @@ function machineIssues(w, m) {
411
431
  severity: "problem",
412
432
  title: `Loaded twice: also in ${other.source} ${other.where}`,
413
433
  short: "Loaded twice",
414
- decision: false,
434
+ // Every repo then runs the other copy instead of yours: a decision, never "fix all".
435
+ decision: true,
415
436
  fixes: [
416
437
  {
417
438
  label: `Keep the ${other.source}'s copy, stop loading yours globally`,
418
- preview: `Copy ${m.name} into your library if needed, then move ${m.path} to the backups.`,
439
+ preview: `Copy ${m.name} into your library if needed, then move ${m.path} to the backups. Every repo then runs the ${other.source}'s copy instead of yours.`,
419
440
  run: (w2) => w2.ops.unloadGlobal(m)
420
441
  }
421
442
  ]
@@ -458,7 +479,16 @@ function issuesOf(w, projectName, u) {
458
479
  preview: s.source === "lib" ? `Delete ${s.dir}/${s.name} in ${projectName}. Your library keeps it.` : `Move ${s.dir}/${s.name} in ${projectName} to Settings \u203A Backups.`,
459
480
  run: (w2) => w2.ops.remove(projectName, s.name)
460
481
  };
461
- if (s.missing)
482
+ if (s.missing && s.library === "missing")
483
+ issues.push({
484
+ id: `unavailable:${s.name}`,
485
+ severity: "problem",
486
+ title: "Pinned in skilllib.json, but not in your library: import it, or set SKILLLIB_HOME to the library that has it",
487
+ short: "Pinned, not in your library",
488
+ decision: true,
489
+ fixes: [{ label: "Remove it from skilllib.json", preview: `${projectName}'s skilllib.json stops pinning ${s.name} v${s.version}. There's no folder to delete.`, run: (w2) => w2.ops.remove(projectName, s.name) }]
490
+ });
491
+ else if (s.missing)
462
492
  issues.push({
463
493
  id: `missing:${s.name}`,
464
494
  severity: "problem",
@@ -467,7 +497,7 @@ function issuesOf(w, projectName, u) {
467
497
  decision: false,
468
498
  fixes: [{ label: "Restore it (sync)", preview: `Reinstall ${s.name} v${s.version} into ${s.dir}.`, run: (w2) => w2.ops.restore(projectName, s.name) }]
469
499
  });
470
- if (s.source === "lib" && s.library === "missing")
500
+ else if (s.source === "lib" && s.library === "missing")
471
501
  issues.push({
472
502
  id: `not-in-lib:${s.name}`,
473
503
  severity: "problem",
@@ -487,7 +517,7 @@ function issuesOf(w, projectName, u) {
487
517
  ...s.source !== "repo" ? [{ ...remove, label: "Remove this repo's copy, keep it global" }] : [],
488
518
  {
489
519
  label: "Stop loading it globally after all",
490
- preview: `Copy ${s.name} into your library if needed, then move ${g.path} to the backups. Other repos stop seeing it.`,
520
+ preview: `Copy ${s.name} into your library if needed, then move ${g.path} to the backups. Every other repo stops seeing it${usedElsewhere(w, projectName, s.name)}.`,
491
521
  run: (w2) => w2.ops.unloadGlobal(g)
492
522
  }
493
523
  ]
@@ -498,17 +528,28 @@ function issuesOf(w, projectName, u) {
498
528
  severity: "problem",
499
529
  title: `Loaded twice: also global in ${g.where}`,
500
530
  short: "Loaded twice",
501
- decision: false,
531
+ // Unloading the global copy changes every repo: a decision, never "fix all".
532
+ decision: true,
502
533
  fixes: [
503
534
  {
504
535
  label: "Keep this repo's copy, stop loading it globally",
505
- preview: `Copy ${s.name} into your library if needed, then move ${g.path} to the backups. Other repos stop seeing it.`,
536
+ preview: `Copy ${s.name} into your library if needed, then move ${g.path} to the backups. Every other repo stops seeing it${usedElsewhere(w, projectName, s.name)}.`,
506
537
  run: (w2) => w2.ops.unloadGlobal(g)
507
538
  },
508
539
  ...s.source !== "repo" ? [{ ...remove, label: "Keep it global, remove it from this repo" }] : []
509
540
  ]
510
541
  });
511
- if (vendor)
542
+ const cursorOnly = vendor?.source === "plugin" && w.agents.includes("claude-code") && !vendor.agents.includes("claude-code");
543
+ if (vendor && cursorOnly)
544
+ issues.push({
545
+ id: `twice-p:${s.name}`,
546
+ severity: "warning",
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`,
548
+ short: "Cursor also lists a plugin's copy",
549
+ decision: true,
550
+ fixes: [pluginFixFor(w, vendor.where)]
551
+ });
552
+ else if (vendor)
512
553
  issues.push({
513
554
  id: `twice-p:${s.name}`,
514
555
  severity: "problem",
@@ -519,7 +560,7 @@ function issuesOf(w, projectName, u) {
519
560
  ...s.source !== "repo" ? [{ ...remove, label: `Remove this repo's copy, use the ${vendor.source}'s` }] : [],
520
561
  vendor.source === "plugin" ? {
521
562
  label: `Turn the plugin off in ${projectName} only`,
522
- preview: `Sets "${vendor.where}": false in ${projectName}'s .claude/settings.local.json (claude plugin disable --scope local). Other repos keep it.`,
563
+ preview: `Sets "${vendor.where}": false in ${projectName}'s .claude/settings.local.json (claude plugin disable --scope local), a local-only file git ignores. Other repos keep it.${vendor.agents.includes("cursor") ? " Cursor ignores repo settings, so it still lists both." : ""}`,
523
564
  run: (w2) => w2.ops.pluginOffHere(projectName, vendor.where)
524
565
  } : {
525
566
  label: `Turn it off in ${vendor.where}`,
@@ -552,21 +593,34 @@ function issuesOf(w, projectName, u) {
552
593
  fixes: [{ label: `Update to v${l.latest}`, preview: `Replace ${s.dir}/${s.name} with library v${l.latest}.`, run: (w2) => w2.ops.update(projectName, s.name) }]
553
594
  });
554
595
  const blind = w.agents.filter((a) => !u.agents.includes(a));
555
- if (!s.missing && blind.length)
596
+ const unshared = blind.filter((a) => s.unshared?.includes(a));
597
+ const linkable = blind.filter((a) => !unshared.includes(a));
598
+ const commitFirst = unshared.length ? ` ${agentNames(unshared)} ${unshared.length === 1 ? "needs" : "need"} it committed first (git tracks the folder they read).` : "";
599
+ if (!s.missing && linkable.length)
556
600
  issues.push({
557
601
  id: `blind:${s.name}`,
558
602
  severity: "warning",
559
- title: `${blind.map((a) => harness(a).name).join(", ")} can't see it`,
560
- short: `${blind.map((a) => harness(a).name).join(", ")} can't see it`,
603
+ title: `${agentNames(blind)} can't see it`,
604
+ short: `${agentNames(blind)} can't see it`,
561
605
  decision: false,
562
606
  fixes: [
563
607
  {
564
608
  label: "Link it for every agent",
565
- preview: `Add links (nothing is copied or moved), so ${blind.map((a) => harness(a).name).join(", ")} load it too.`,
609
+ preview: `Add links (nothing is copied or moved), so ${agentNames(linkable)} ${linkable.length === 1 ? "loads" : "load"} it too.${commitFirst}`,
566
610
  run: (w2) => w2.ops.link(projectName, s.name)
567
611
  }
568
612
  ]
569
613
  });
614
+ else if (!s.missing && unshared.length)
615
+ issues.push({
616
+ id: `blind:${s.name}`,
617
+ severity: "warning",
618
+ title: `${agentNames(unshared)} can't see it: commit ${s.dir}/${s.name} first, then link it (git tracks the folder they read, and a link to an uncommitted copy would break for teammates)`,
619
+ short: `${agentNames(unshared)} can't see it: commit it first`,
620
+ // Nothing skilllib can do: committing is yours.
621
+ decision: true,
622
+ fixes: []
623
+ });
570
624
  const differ = !!s.dupes?.differ;
571
625
  if (s.source !== "lib" && s.library === "differs" && !differ)
572
626
  issues.push({
@@ -586,7 +640,8 @@ function issuesOf(w, projectName, u) {
586
640
  severity: "hint",
587
641
  title: "Same as your library skill, but not tracked",
588
642
  short: "Not tracked",
589
- decision: false,
643
+ // It writes to the repo's skilllib.json (and your library): your call, never in Fix all (as in doctor, #11).
644
+ decision: true,
590
645
  fixes: [{ label: "Track it", preview: `Record ${s.name} in skilllib.json so library updates reach it.`, run: (w2) => w2.ops.track(projectName, s.name) }]
591
646
  });
592
647
  if (s.source === "untracked" && !l && !vendor && !differ)
@@ -595,7 +650,8 @@ function issuesOf(w, projectName, u) {
595
650
  severity: "hint",
596
651
  title: "Only exists in this repo",
597
652
  short: "Only in this repo",
598
- decision: false,
653
+ // It writes to the repo's skilllib.json (and your library): your call, never in Fix all (as in doctor, #11).
654
+ decision: true,
599
655
  fixes: [
600
656
  {
601
657
  label: "Import it into your library",
@@ -604,7 +660,7 @@ function issuesOf(w, projectName, u) {
604
660
  }
605
661
  ]
606
662
  });
607
- if (s.source === "lib" && w.days && u.uses === 0 && !s.missing)
663
+ if (s.source === "lib" && w.days && u.uses === 0 && !s.missing && (s.added ?? 0) <= Date.now() - 30 * 24 * 60 * 60 * 1e3)
608
664
  issues.push({
609
665
  id: `unused:${s.name}`,
610
666
  severity: "hint",
@@ -615,6 +671,53 @@ function issuesOf(w, projectName, u) {
615
671
  });
616
672
  return issues;
617
673
  }
674
+ function agentSkillFix(w) {
675
+ return {
676
+ label: w.agentSkill === "outdated" ? "Update the skilllib skill" : "Install the skilllib skill for your agents",
677
+ preview: "One global skill, skilllib, where your agents look in every repo (~/.claude/skills, and ~/.agents/skills for Codex and Zed). Then ask your agents which skills they can use here, where each comes from, and which of your skills a repo should add. `skilllib agent-skill remove` takes it out.",
678
+ run: (w2) => w2.ops.installAgentSkill()
679
+ };
680
+ }
681
+ function removeAgentSkillFix() {
682
+ return {
683
+ label: "Remove the skilllib skill",
684
+ preview: "Removes it from ~/.claude/skills and ~/.agents/skills, where skilllib put it. Your agents then stop using skilllib to answer which skills they can use. Settings \u203A skilllib skill puts it back.",
685
+ run: (w) => w.ops.removeAgentSkill()
686
+ };
687
+ }
688
+ function agentSkillIssue(w) {
689
+ if (w.agentSkill === "installed" || !w.agents.length) return null;
690
+ const missing = w.agentSkill === "missing";
691
+ return {
692
+ id: "agent-skill",
693
+ severity: missing ? "hint" : "warning",
694
+ title: missing ? "Your agents don't know about skilllib" : "The skilllib skill for your agents is out of date",
695
+ short: missing ? "Your agents don't know about skilllib" : "Out of date",
696
+ decision: missing,
697
+ fixes: [agentSkillFix(w)]
698
+ };
699
+ }
700
+ function runAll(w, fixes) {
701
+ const held = [];
702
+ const bad = [];
703
+ for (const fix of fixes) {
704
+ let r;
705
+ try {
706
+ r = fix.run(w);
707
+ } catch (e) {
708
+ r = failed(e.message);
709
+ }
710
+ if (isFailure(r)) bad.push(said(r));
711
+ else if (followUp(r)) held.push(said(r));
712
+ }
713
+ const ok = fixes.length - held.length - bad.length;
714
+ const message = [
715
+ `Applied ${ok}${ok === fixes.length ? "" : ` of ${fixes.length}`} fix${fixes.length === 1 ? "" : "es"}${ok ? " (anything removed is in Settings \u203A Backups)" : ""}`,
716
+ held.length ? `${held.join("; ")} (fix ${held.length === 1 ? "it" : "them"} on ${held.length === 1 ? "its" : "their"} own to go ahead)` : "",
717
+ bad.length ? `couldn't: ${bad.join("; ")}` : ""
718
+ ].filter(Boolean).join(" \xB7 ");
719
+ return bad.length ? failed(message) : message;
720
+ }
618
721
  function worst(issues) {
619
722
  return [...issues].sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity])[0];
620
723
  }
@@ -643,28 +746,120 @@ function deleteLibraryFix(w, names) {
643
746
  "Every copy goes to Settings \u203A Backups.",
644
747
  others.length ? `Untracked or the team's own copies in ${others.join(", ")} stay.` : ""
645
748
  ].filter(Boolean).join(" "),
646
- run: (w2) => names.map((n) => w2.ops.deleteLibrary(n)).map((r) => typeof r === "string" ? r : r.message).join(" \xB7 ")
749
+ run: (w2) => joined(names.map((n) => w2.ops.deleteLibrary(n)))
647
750
  };
648
751
  }
649
- function replacePluginFix(w, id) {
650
- const skills = w.machine.filter((m) => m.source === "plugin" && m.where === id);
651
- const names = skills.map((m) => m.name);
652
- const parts = skills[0]?.pluginParts ?? [];
653
- 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;
654
769
  return {
655
- label: `Replace ${id.split("@")[0]} with library skills\u2026`,
770
+ label: `Replace ${pluginName(p)} with library skills\u2026`,
656
771
  preview: [
657
- `Copies its ${skills.length} skill${skills.length === 1 ? "" : "s"} into your library and adds them to the repos you tick (or none),`,
658
- 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).`,
659
- parts.length ? `It also brings ${parts.join(", ")}: those ${synced ? "stop" : "go"} too.` : "",
660
- 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."
661
776
  ].filter(Boolean).join(" "),
662
777
  run: () => "",
663
778
  allowNone: true,
664
- preticked: (w2) => w2.projects.filter((p) => names.some((n) => (w2.usage[p.name]?.[n] ?? 0) > 0)).map((p) => p.name),
665
- 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)
666
795
  };
667
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
+ }
668
863
  function originGroup(origin) {
669
864
  if (!origin) return null;
670
865
  const plugin = origin.match(/^plugin: (.+)$/);
@@ -689,7 +884,9 @@ function groupCandidates(w, u) {
689
884
  if (m?.source === "plugin") return [{ key: `plugin:${m.where}`, label: `\u29C9 ${m.where.split("@")[0]} plugin` }];
690
885
  if (m?.source === "claude.ai") return [{ key: "claude.ai", label: "claude.ai skills" }];
691
886
  if (m?.source === "cursor") return [{ key: "cursor", label: "Cursor built-ins" }];
887
+ if (m?.source === "grok") return [{ key: "grok", label: "Grok bundled" }];
692
888
  if (m?.source === "system") return [{ key: `system:${m.where}`, label: m.where }];
889
+ if (m?.source === "skilllib") return [];
693
890
  if (m) {
694
891
  const origin = originGroup(m.origin);
695
892
  return [...origin ? [origin] : [], ...byTime("global", m.installed), ...byWord("global", m.name)];
@@ -733,10 +930,8 @@ function addRows(w, target, q) {
733
930
  name: q ? `+ Create "${q}"${where}` : "+ Create a new skill (type its name)",
734
931
  note: "",
735
932
  description: "",
736
- run: (w2) => {
737
- const made = w2.ops.createSkill(q);
738
- return target ? w2.ops.add(target, q) : made;
739
- }
933
+ run: (w2) => w2.ops.createSkill(q),
934
+ ...target ? { afterEdit: (w2) => w2.ops.add(target, q) } : {}
740
935
  },
741
936
  { key: "agent", agent: true, name: q ? `\u2726 Write "${q}" with an agent` : "\u2726 Write a new skill with an agent", note: "", description: "" }
742
937
  ];
@@ -745,9 +940,32 @@ function addRows(w, target, q) {
745
940
  }
746
941
 
747
942
  // src/tui/prompts.ts
748
- function reviewPrompt(w, names, scope) {
943
+ var agentNames2 = (agents) => agents.map((a) => harness(a).name).join(", ") || "none of my agents";
944
+ var localSource = (s) => s.source === "lib" ? "Your skills (skilllib library), installed in the repo" : s.source === "repo" ? "a repo's own skill (committed by the team)" : "a copy only on this machine (not committed)";
945
+ var localInstalled = (s) => s.origin ?? (s.source === "lib" ? "added from my library with skilllib" : s.source === "repo" ? "committed in the repo" : "copied into the repo by hand (not committed)");
946
+ function reviewPrompt(w, names, scope, repo) {
749
947
  const skills = names.map((name) => {
750
948
  const copies = w.machine.filter((m2) => m2.name === name);
949
+ const here = repo ? w.projects.find((p) => p.name === repo)?.skills.find((s) => s.name === name) : void 0;
950
+ if (here) {
951
+ const lib3 = w.library.find((l) => l.name === name);
952
+ return {
953
+ name,
954
+ description: w.descriptions[name] ?? "",
955
+ source: localSource(here),
956
+ installedHow: localInstalled(here),
957
+ path: here.path,
958
+ links: [],
959
+ loadedBy: `${agentNames2(here.agents)} \u2014 in ${repo}`,
960
+ vendor: null,
961
+ usesTotal: w.days ? totalUses(w, name) : null,
962
+ usesByProject: Object.entries(w.usage).flatMap(([p, u]) => u[name] ? [[p, u[name]]] : []),
963
+ installedIn: w.projects.filter((p) => p.skills.some((s) => s.name === name)).map((p) => p.name),
964
+ otherCopies: copies.map((c) => `${c.source} ${c.where}`),
965
+ inYourSkills: lib3 ? `yes, v${lib3.latest}` : null,
966
+ keptGlobal: void 0
967
+ };
968
+ }
751
969
  const m = copies.find((c) => c.source === "global") ?? copies[0];
752
970
  const lib2 = w.library.find((l) => l.name === name);
753
971
  const repos = w.projects.filter((p) => p.skills.some((s) => s.name === name));
@@ -755,11 +973,11 @@ function reviewPrompt(w, names, scope) {
755
973
  return {
756
974
  name,
757
975
  description: w.descriptions[name] ?? "",
758
- source: m ? m.source === "global" ? "my global skills" : `vendor: ${m.source} (${m.where})` : lib2 ? "Your skills (skilllib library)" : "a repo's own skill",
759
- installedHow: m?.origin ?? (m ? m.where : lib2 ? "created or imported into skilllib" : "committed in the repo"),
976
+ source: m ? m.source === "global" ? "my global skills" : `vendor: ${m.source} (${m.where})` : lib2 ? "Your skills (skilllib library)" : local ? localSource(local) : "a repo's own skill",
977
+ installedHow: m?.origin ?? (m ? m.where : lib2 ? "created or imported into skilllib" : local ? localInstalled(local) : "committed in the repo"),
760
978
  path: m?.path ?? (lib2 ? w.ops.libraryFile(name).replace(/\/SKILL\.md$/, "") : local?.path ?? ""),
761
979
  links: m?.links ?? [],
762
- loadedBy: m ? `${m.agents.map((a) => harness(a).name).join(", ") || "none of my agents"} \u2014 in every repo` : `repos: ${repos.map((p) => p.name).join(", ") || "none"}`,
980
+ loadedBy: m ? `${agentNames2(m.agents)} \u2014 in every repo` : repos.map((p) => `${agentNames2(p.skills.find((s) => s.name === name).agents)} \u2014 in ${p.name}`).join("; ") || "none of my agents",
763
981
  vendor: m && m.source !== "global" ? "turn it off at its source (/plugin in Claude Code, claude.ai settings, or Cursor)" : null,
764
982
  usesTotal: w.days ? totalUses(w, name) : null,
765
983
  usesByProject: Object.entries(w.usage).flatMap(([p, u]) => u[name] ? [[p, u[name]]] : []),
@@ -792,13 +1010,12 @@ Keep it under 200 lines. ${p ? "skilllib will offer to import it into my library
792
1010
 
793
1011
  // src/tui/App.tsx
794
1012
  import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
795
- function tildify2(p) {
796
- return p.startsWith(userHome()) ? "~" + p.slice(userHome().length) : p;
797
- }
1013
+ var tildify3 = (p) => tildify(p);
798
1014
  var PLACES = [
799
1015
  ["projects", "Projects"],
800
1016
  ["library", "Your skills"],
801
1017
  ["global", "Global"],
1018
+ ["plugins", "Plugins"],
802
1019
  ["health", "Health"],
803
1020
  ["settings", "Settings"]
804
1021
  ];
@@ -826,6 +1043,7 @@ function tag(u, w) {
826
1043
  if (s) return [s.source, color.blue];
827
1044
  if (u.source === "global") return [u.machine?.kept ? "global \u2713" : "global", color.yellow];
828
1045
  if (u.source === "plugin") return [`\u29C9 ${u.where.split("@")[0]}`, color.magenta];
1046
+ if (u.source === "skilllib") return ["\u25C6 skilllib", color.accent];
829
1047
  return [u.source, color.muted];
830
1048
  }
831
1049
  function isNameChar(input, key) {
@@ -876,7 +1094,7 @@ function TitleRow({ cells, width }) {
876
1094
  }
877
1095
  var fixOption = (fix) => ({ label: fix.label, fix });
878
1096
  var GIT_LABEL = { committed: "\u2713 committed", changed: "\xB1 uncommitted changes", new: "+ not committed", ignored: "\u2205 gitignored" };
879
- var COPY_RANK = { lib: 0, repo: 0, untracked: 0, global: 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 };
880
1098
  function byName(copies) {
881
1099
  const groups = /* @__PURE__ */ new Map();
882
1100
  for (const c of copies) groups.set(c.name, [...groups.get(c.name) ?? [], c]);
@@ -944,7 +1162,7 @@ function skillItem(w, copies, issues, nameW, actions, repo) {
944
1162
  const b = builder(u.name);
945
1163
  for (const l of wrap(w.descriptions[u.name] || "(no description)", width - 4, 3)) b.line([l, color.muted]);
946
1164
  b.line();
947
- if (repo && !u.local) b.line(["Loads in every repo", color.text], [u.source === "global" ? " issues and cleanup live in Global" : " managed at its source", color.faint]);
1165
+ if (repo && !u.local) b.line(["Loads in every repo", color.text], [u.source === "global" || u.source === "skilllib" ? " issues and cleanup live in Global" : " managed at its source", color.faint]);
948
1166
  else b.issues(issues);
949
1167
  b.actions(actions, width);
950
1168
  b.line();
@@ -953,7 +1171,7 @@ function skillItem(w, copies, issues, nameW, actions, repo) {
953
1171
  b.line(["Loaded from", color.accent]);
954
1172
  for (const c of copies) {
955
1173
  const s = c.local;
956
- const path = s ? `${s.dir}/${c.name}` : c.source === "global" ? `${c.where}/${c.name}` : c.where;
1174
+ const path = s ? `${s.dir}/${c.name}` : c.source === "global" || c.source === "skilllib" ? `${c.where}/${c.name}` : c.where;
957
1175
  const extra = s ? s.missing ? "missing" : s.git ? GIT_LABEL[s.git] : "" : "";
958
1176
  const [ct, cc] = c === u ? ["", void 0] : tag(c, w);
959
1177
  b.line([" ", void 0], [path + " ", color.text], [ct ? ct + " " : "", cc], [extra, color.faint]);
@@ -967,7 +1185,10 @@ function projectSkillItems(w, p, nameW, ui) {
967
1185
  (copies) => skillItem(w, copies, mergeIssues(copies.filter((c) => c.local).map((c) => issuesOf(w, p, c))), nameW, skillActions(w, p, copies[0], ui), p)
968
1186
  );
969
1187
  }
970
- var reviewOption = (w, name, ui) => ({ label: "Review prompt", action: () => ui.copy(reviewPrompt(w, [name], `Review the skill ${name}.`), `a review prompt for ${name}`) });
1188
+ var reviewOption = (w, name, ui, repo) => ({
1189
+ label: "Review prompt",
1190
+ action: () => ui.copy(reviewPrompt(w, [name], `Review the skill ${name}${repo ? ` in ${repo}` : ""}.`, repo), `a review prompt for ${name}`)
1191
+ });
971
1192
  function versionsOption(w, repo, name, installed, ui) {
972
1193
  return {
973
1194
  label: "Other versions\u2026",
@@ -980,27 +1201,35 @@ function versionsOption(w, repo, name, installed, ui) {
980
1201
  function skillActions(w, p, u, ui) {
981
1202
  const s = u.local;
982
1203
  const inLibrary = w.library.find((l) => l.name === u.name);
983
- const remove = { label: "Remove from repo", preview: s?.source === "lib" ? `Delete ${s.dir}/${u.name} in ${p}. Your library keeps it.` : `Move ${s?.dir}/${u.name} in ${p} to Settings \u203A Backups.`, run: (w2) => w2.ops.remove(p, u.name) };
1204
+ const remove = {
1205
+ // Its folder gone: all that's left to remove is the skilllib.json entry (the label Health offers too).
1206
+ label: s?.missing ? "Remove it from skilllib.json" : "Remove from repo",
1207
+ preview: s?.missing ? `${p}'s skilllib.json stops pinning ${u.name}. There's no folder to delete.` : s?.source === "lib" ? `Delete ${s.dir}/${u.name} in ${p}.${inLibrary ? " Your library keeps it." : ""}` : `Move ${s?.dir}/${u.name} in ${p} to Settings \u203A Backups.`,
1208
+ run: (w2) => w2.ops.remove(p, u.name)
1209
+ };
984
1210
  if (s?.source === "lib")
985
1211
  return [
986
- ...inLibrary && s.version < inLibrary.latest ? [fixOption({ label: `Update to v${inLibrary.latest}`, preview: `Replace ${s.dir}/${u.name} with library v${inLibrary.latest}.`, run: (w2) => w2.ops.update(p, u.name) })] : [],
987
- { label: "Edit in library", action: () => ui.edit(w.ops.libraryFile(u.name)) },
988
- versionsOption(w, p, u.name, s.version, ui),
1212
+ // A tracked skill your library doesn't have: nothing to edit there, no versions to pick.
1213
+ ...inLibrary ? [
1214
+ ...s.version < inLibrary.latest ? [fixOption({ label: `Update to v${inLibrary.latest}`, preview: `Replace ${s.dir}/${u.name} with library v${inLibrary.latest}.`, run: (w2) => w2.ops.update(p, u.name) })] : [],
1215
+ { label: "Edit in library", action: () => ui.edit(w.ops.libraryFile(u.name)) },
1216
+ versionsOption(w, p, u.name, s.version, ui)
1217
+ ] : [],
989
1218
  fixOption(remove),
990
- reviewOption(w, u.name, ui)
1219
+ reviewOption(w, u.name, ui, p)
991
1220
  ];
992
1221
  if (s?.source === "repo")
993
1222
  return [
994
1223
  ...inLibrary ? [] : [fixOption({ label: "Copy into library", preview: `Copy ${u.name} into your library, so other repos can add it. The repo's copy stays as it is.`, run: (w2) => w2.ops.copyToLibrary(p, u.name) })],
995
1224
  { label: "Edit SKILL.md", action: () => ui.edit(`${s.path}/SKILL.md`) },
996
- reviewOption(w, u.name, ui)
1225
+ reviewOption(w, u.name, ui, p)
997
1226
  ];
998
1227
  if (s)
999
1228
  return [
1000
1229
  inLibrary ? fixOption({ label: "Track it", preview: `Record ${u.name} in skilllib.json so library updates reach it.`, run: (w2) => w2.ops.track(p, u.name) }) : fixOption({ label: "Import it into your library", preview: `Copy ${u.name} into your library and track it here.`, run: (w2) => w2.ops.importLocal(p, u.name) }),
1001
1230
  { label: "Edit SKILL.md", action: () => ui.edit(`${s.path}/SKILL.md`) },
1002
1231
  fixOption(remove),
1003
- reviewOption(w, u.name, ui)
1232
+ reviewOption(w, u.name, ui, p)
1004
1233
  ];
1005
1234
  return [{ label: "Open in Global", action: () => ui.openInGlobal(u.name) }, reviewOption(w, u.name, ui)];
1006
1235
  }
@@ -1008,11 +1237,13 @@ function overview(w, p) {
1008
1237
  const project2 = project(w, p);
1009
1238
  const info = w.ops.repoInfo(p);
1010
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]))];
1011
1241
  return [
1012
- ["Folder", [[tildify2(project2.path), color.text]]],
1242
+ ["Folder", [[tildify3(project2.path), color.text]]],
1013
1243
  ["Remote", [[info.remote ?? "none", info.remote ? color.blue : color.faint]]],
1014
1244
  ["Branch", [[info.branch ?? "\u2013", color.text], [info.dirty ? ` ${info.dirty} uncommitted` : "", color.yellow]]],
1015
- ["Manifest", [manifest]]
1245
+ ["Manifest", [manifest]],
1246
+ ["Plugins", [[plugins.length ? plugins.join(", ") : "none", plugins.length ? color.magenta : color.faint]]]
1016
1247
  ];
1017
1248
  }
1018
1249
  function InfoGrid({ rows, columns, width }) {
@@ -1054,13 +1285,21 @@ function globalItems(w, nameW, ui) {
1054
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 }));
1055
1286
  return byName(copies).map((g) => skillItem(w, g, mergeIssues(g.map((c) => machineIssues(w, c.machine))), nameW, globalActions(w, g[0], ui), null));
1056
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
+ };
1057
1296
  function globalActions(w, u, ui) {
1058
1297
  const m = u.machine;
1059
- 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];
1060
1299
  return [
1061
1300
  ...machineActions(m).map(fixOption),
1062
- ...m.source === "plugin" ? [fixOption(replacePluginFix(w, m.where))] : [],
1063
- ...m.source === "global" ? [] : [{ 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`) }] : [],
1064
1303
  ...m.source === "global" && !m.broken ? [{ label: "Edit SKILL.md", action: () => ui.edit(`${m.path}/SKILL.md`) }] : [],
1065
1304
  reviewOption(w, m.name, ui)
1066
1305
  ];
@@ -1068,8 +1307,9 @@ function globalActions(w, u, ui) {
1068
1307
  function libraryItems(w, nameW, ui) {
1069
1308
  return w.library.map((l) => {
1070
1309
  const repos = w.projects.filter((p) => p.skills.some((s) => s.name === l.name));
1310
+ const removable = repos.filter((p) => p.skills.some((s) => s.name === l.name && s.source !== "repo"));
1071
1311
  const uses = totalUses(w, l.name);
1072
- const addFix = { label: "Add to repos\u2026", preview: `Install ${l.name} v${l.latest} into the repos you pick.`, run: () => "", candidates: (w2) => w2.projects.filter((p) => !p.skills.some((s) => s.name === l.name)).map((p) => p.name), pickRepos: (w2, rs) => rs.map((r) => said(w2.ops.add(r, l.name))).join(" \xB7 ") };
1312
+ const addFix = { label: "Add to repos\u2026", preview: `Install ${l.name} v${l.latest} into the repos you pick.`, run: () => "", candidates: (w2) => w2.projects.filter((p) => !p.skills.some((s) => s.name === l.name)).map((p) => p.name), pickRepos: (w2, rs) => w2.ops.addTo(rs, [l.name]) };
1073
1313
  const delFix = deleteLibraryFix(w, [l.name]);
1074
1314
  const issues = repos.length ? [] : [{ id: `nowhere:${l.name}`, severity: "hint", title: "Used in no repo", short: "Used in no repo", decision: true, fixes: [addFix, delFix] }];
1075
1315
  const behind = repos.filter((p) => p.skills.some((s) => s.name === l.name && s.source === "lib" && s.version < l.latest)).length;
@@ -1101,13 +1341,13 @@ function libraryItems(w, nameW, ui) {
1101
1341
  b.issues(issues);
1102
1342
  b.actions([
1103
1343
  fixOption(addFix),
1104
- ...repos.length ? [
1344
+ ...removable.length ? [
1105
1345
  fixOption({
1106
1346
  label: "Remove from repos\u2026",
1107
1347
  preview: `Pick repos to remove ${l.name} from. Your library keeps it.`,
1108
1348
  run: () => "",
1109
- candidates: () => repos.map((p) => p.name),
1110
- pickRepos: (w2, rs) => rs.map((r) => said(w2.ops.remove(r, l.name))).join(" \xB7 ")
1349
+ candidates: () => removable.map((p) => p.name),
1350
+ pickRepos: (w2, rs) => joined(rs.map((r) => w2.ops.remove(r, l.name)))
1111
1351
  })
1112
1352
  ] : [],
1113
1353
  fixOption(delFix),
@@ -1123,15 +1363,99 @@ function libraryItems(w, nameW, ui) {
1123
1363
  }
1124
1364
  function healthItems(w, nameW, ui) {
1125
1365
  const flagged = (where, items) => items.filter((i) => i.issues.length).map((i) => ({ ...i, key: `${where}:${i.key}`, cells: [i.cells[0], i.cells[1], { text: clip(where, 16), width: 16, color: color.muted }, i.cells[4]] }));
1126
- return [...w.projects.flatMap((p) => flagged(p.name, projectSkillItems(w, p.name, nameW, ui))), ...flagged("Global", globalItems(w, nameW, ui))];
1366
+ return [...w.projects.flatMap((p) => flagged(p.name, projectSkillItems(w, p.name, nameW, ui))), ...flagged("Global", globalItems(w, nameW, ui)), ...agentSkillItems(w, nameW)];
1367
+ }
1368
+ function agentSkillItems(w, nameW) {
1369
+ const issue = agentSkillIssue(w);
1370
+ if (!issue || w.machine.some((m) => m.source === "skilllib")) return [];
1371
+ const sev = SEV[issue.severity];
1372
+ return [
1373
+ {
1374
+ key: "agent-skill",
1375
+ name: "skilllib",
1376
+ issues: [issue],
1377
+ uses: 0,
1378
+ cells: [
1379
+ { text: sev.icon, width: 2, color: sev.color },
1380
+ { text: clip("skilllib", nameW), width: nameW },
1381
+ { text: clip("Your agents", 16), width: 16, color: color.muted },
1382
+ { text: " " + issue.short, grow: true, color: sev.color }
1383
+ ],
1384
+ detail: (width) => {
1385
+ const b = builder("skilllib");
1386
+ for (const l of wrap("The skilllib skill lets you ask your agents which skills they can use here, where each comes from, and which of your skills a repo should add.", width - 4, 3)) b.line([l, color.muted]);
1387
+ b.line();
1388
+ b.issues([issue]);
1389
+ return b.d;
1390
+ }
1391
+ }
1392
+ ];
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
+ });
1127
1453
  }
1128
1454
  function addCursor(rows, cursor) {
1129
1455
  const i = Math.min(cursor, rows.length - 1);
1130
1456
  return rows[i] && !rows[i].header ? i : Math.max(0, rows.findIndex((r) => !r.header));
1131
1457
  }
1132
- function said(r) {
1133
- return typeof r === "string" ? r : r.message;
1134
- }
1458
+ var said2 = said;
1135
1459
  function App({ initial, reload, loadUsage: loadUsage2 }) {
1136
1460
  const { exit, suspendTerminal } = useApp();
1137
1461
  const { columns: termCols, rows: termRows } = useWindowSize();
@@ -1168,17 +1492,21 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1168
1492
  const ui = {
1169
1493
  notify: setToast,
1170
1494
  openInGlobal,
1495
+ openInPlugins,
1171
1496
  edit,
1172
- copy: (text, what) => setToast(said(world.ops.copy(text, what))),
1497
+ copy: (text, what) => setToast(said2(world.ops.copy(text, what))),
1173
1498
  menu: (title, options) => setModal({ kind: "menu", title, options, cursor: 0 })
1174
1499
  };
1500
+ const pluginIssues2 = new Map(world.plugins.map((p) => [p.key, pluginIssues(world, p)]));
1175
1501
  const flagged = [
1176
1502
  ...world.projects.flatMap((p) => projectIssues(world, p.name).map((x) => ({ key: `${p.name}:${x.skill.name}`, issue: x.issue }))),
1177
- ...world.machine.flatMap((m) => machineIssues(world, m).map((issue) => ({ key: `Global:${m.name}`, issue })))
1503
+ ...world.machine.flatMap((m) => machineIssues(world, m).map((issue) => ({ key: `Global:${m.name}`, issue }))),
1504
+ // The skilllib skill's issue is on its Global row when it has one (see agentSkillItems).
1505
+ ...[agentSkillIssue(world)].flatMap((issue) => issue ? [{ key: "Global:skilllib", issue }] : [])
1178
1506
  ];
1179
1507
  const healthCount = new Set(flagged.map((f) => f.key)).size;
1180
1508
  const healthWorst = worst(flagged.map((f) => f.issue));
1181
- 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();
1182
1510
  const inChip = (i, c) => c === "all" || (c === "issues" ? i.issues.length > 0 : i.group === c);
1183
1511
  let items = all.filter((i) => i.header || (!query || matches(i.name, query)) && inChip(i, chip));
1184
1512
  const viewKey = [place, open, chip, query, used ? "usage" : ""].join("|");
@@ -1203,16 +1531,18 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1203
1531
  if (items[cursor]?.header) cursor = firstSelectable;
1204
1532
  const current = items[cursor];
1205
1533
  const detail = current?.detail?.(columns);
1206
- function apply(run) {
1534
+ function apply(run, w = world) {
1207
1535
  let result;
1208
1536
  try {
1209
- result = run(world);
1537
+ result = run(w);
1210
1538
  } catch (e) {
1211
- result = `Couldn't do it: ${e.message}`;
1539
+ result = failed(`Couldn't do it: ${e.message}`);
1212
1540
  }
1213
1541
  setBase(reload());
1214
- setToast(`\u2713 ${said(result)}`);
1215
- if (typeof result !== "string") setModal({ kind: "confirm", fix: result.then });
1542
+ const then = followUp(result);
1543
+ const toast2 = `${isFailure(result) ? "\u2717" : then ? "!" : "\u2713"} ${said2(result)}`;
1544
+ setToast(toast2);
1545
+ if (then) setModal({ kind: "confirm", fix: then, toast: toast2 });
1216
1546
  }
1217
1547
  function choose(o) {
1218
1548
  if (o.action) return o.action();
@@ -1232,6 +1562,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1232
1562
  function fixSelected() {
1233
1563
  const issue = current && worst(current.issues);
1234
1564
  if (!issue) return setToast("Nothing to fix \xB7 enter shows what you can do");
1565
+ if (!issue.fixes.length) return setToast(`Nothing skilllib can do here: ${issue.short}`);
1235
1566
  if (issue.decision) {
1236
1567
  setFocus("detail");
1237
1568
  setDetailCursor(Math.max(0, detail?.options.findIndex((o) => o.issue === issue) ?? 0));
@@ -1308,7 +1639,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1308
1639
  preview: `Pick repos. These ${n(mine.length)} go into your library and those repos, and stop loading globally (originals backed up).`,
1309
1640
  run: () => "",
1310
1641
  preticked: usedIn,
1311
- pickRepos: (w, repos) => (mine.forEach((m) => w.ops.moveGlobal(m, repos, label)), `${n(mine.length)} now load only in ${repos.join(", ")}`)
1642
+ pickRepos: (w, repos) => w.ops.moveGlobal(mine, repos, label)
1312
1643
  }),
1313
1644
  ...mine.some((m) => !m.kept) ? [fixOption({ label: `Keep all ${mine.length} global on purpose`, preview: `skilllib stops warning about these ${n(mine.length)}.`, run: (w) => (mine.forEach((m) => w.ops.keepGlobal(m.name, true)), `${n(mine.length)} marked as global on purpose`) })] : [],
1314
1645
  fixOption({ label: `Delete all ${mine.length}`, preview: `Move these ${n(mine.length)} to Settings \u203A Backups.`, run: (w) => (mine.forEach((m) => w.ops.deleteGlobal(m)), `Deleted ${n(mine.length)} (in Settings \u203A Backups)`) })
@@ -1319,13 +1650,12 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1319
1650
  preview: `Install these ${n(libs.length)} into the repos you pick.`,
1320
1651
  run: () => "",
1321
1652
  preticked: () => [],
1322
- pickRepos: (w, repos) => (repos.forEach((r) => libs.forEach((l) => w.ops.add(r, l.name))), `Added ${n(libs.length)} to ${repos.join(", ")}`)
1653
+ pickRepos: (w, repos) => w.ops.addTo(repos, libs.map((l) => l.name))
1323
1654
  })
1324
1655
  ] : [],
1325
1656
  ...libs.length ? [fixOption(deleteLibraryFix(world, libs.map((l) => l.name)))] : [],
1326
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}`) })] : [],
1327
- ...[...new Set(machines.filter((m) => m.source === "plugin").map((m) => m.where))].map((id) => fixOption(replacePluginFix(world, id))),
1328
- ...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) })),
1329
1659
  { label: "Review prompt", action: () => ui.copy(reviewPrompt(world, names, `Review these related skills (${label}).`), `a review prompt for ${label}`) }
1330
1660
  ];
1331
1661
  }
@@ -1359,9 +1689,9 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1359
1689
  `\u22EF ${repo}\u2026`,
1360
1690
  ["Open its folder, hide it from the list, or copy a review prompt for its skills."],
1361
1691
  () => ui.menu(repo, [
1362
- { label: "Open its folder", action: () => setToast(said(world.ops.openFolder(project(world, repo).path))) },
1692
+ { label: "Open its folder", action: () => setToast(said2(world.ops.openFolder(project(world, repo).path))) },
1363
1693
  { label: "Hide it from the list", action: () => (apply((w) => w.ops.hide(repo)), setOpen(null), setFocus(sidebar ? "sidebar" : "list")) },
1364
- ...skills.length ? [{ label: `Review prompt for its ${skills.length} skills`, action: () => ui.copy(reviewPrompt(world, skills, `Review the skills in ${repo}.`), `a review prompt for ${repo}`) }] : []
1694
+ ...skills.length ? [{ label: `Review prompt for its ${skills.length} skills`, action: () => ui.copy(reviewPrompt(world, skills, `Review the skills in ${repo}.`, repo), `a review prompt for ${repo}`) }] : []
1365
1695
  ])
1366
1696
  )
1367
1697
  ];
@@ -1393,19 +1723,21 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1393
1723
  setQuery("");
1394
1724
  setChip("all");
1395
1725
  }
1396
- function edit(file) {
1397
- if (!existsSync2(file)) return setToast(`${tildify2(file)} doesn't exist`);
1726
+ function edit(file, then) {
1727
+ if (!existsSync2(file)) return then ? apply(then) : (setBase(reload()), setToast(`${tildify3(file)} doesn't exist`));
1398
1728
  const editor = process.env.VISUAL || process.env.EDITOR || (process.platform === "win32" ? "notepad" : "vi");
1399
1729
  void suspendTerminal(() => {
1400
1730
  if (process.platform === "win32") spawnSync2(`${editor} "${file}"`, { shell: true, stdio: "inherit" });
1401
1731
  else spawnSync2("/bin/sh", ["-c", `${editor} "$1"`, "sh", file], { stdio: "inherit" });
1402
1732
  }).then(() => {
1403
- setBase(reload());
1404
- setToast(`Saved ${tildify2(file)}`);
1733
+ const fresh = reload();
1734
+ if (then) return apply(then, fresh);
1735
+ setBase(fresh);
1736
+ setToast(`Saved ${tildify3(file)}`);
1405
1737
  });
1406
1738
  }
1407
1739
  function askForFolder(title) {
1408
- setModal({ kind: "input", title, value: "~/Projects", submit: (v) => apply((w) => w.ops.addRoot(v)) });
1740
+ setModal({ kind: "input", title, value: "", placeholder: "~/Projects", submit: (v) => apply((w) => w.ops.addRoot(v)) });
1409
1741
  }
1410
1742
  useEffect(() => {
1411
1743
  const folder = () => !initial.roots.length && askForFolder("Where are your projects? skilllib finds the git repos in there");
@@ -1416,6 +1748,10 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1416
1748
  goPlace("global");
1417
1749
  setQuery(name);
1418
1750
  }
1751
+ function openInPlugins(id) {
1752
+ goPlace("plugins");
1753
+ setQuery(id);
1754
+ }
1419
1755
  function settingsItems() {
1420
1756
  const header = (key, text) => ({ key, name: "", header: true, issues: [], uses: 0, cells: [{ text }] });
1421
1757
  const row = (key, text, sub, onEnter, c) => ({
@@ -1434,20 +1770,27 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1434
1770
  apply((w) => (w.ops.setAgents(wasOn ? w.agents.filter((a) => a !== h.id) : HARNESSES.map((x) => x.id).filter((id) => id === h.id || w.agents.includes(id))), `${h.name} ${wasOn ? "off" : "on"}`));
1435
1771
  })
1436
1772
  ),
1773
+ // The skill that lets your agents use skilllib: installing adds a global skill, so it asks first.
1774
+ row(
1775
+ "agent-skill",
1776
+ `${world.agentSkill === "installed" ? "[x]" : "[ ]"} \u25C6 skilllib skill`,
1777
+ world.agentSkill === "installed" ? "your agents can use skilllib" : world.agentSkill === "outdated" ? "out of date \xB7 enter updates it" : "lets your agents use skilllib \xB7 enter installs it",
1778
+ () => world.agentSkill === "installed" ? setToast("Installed \xB7 `skilllib agent-skill remove` takes it out") : choose(fixOption(agentSkillFix(world)))
1779
+ ),
1437
1780
  header("#roots", "Project folders"),
1438
1781
  ...world.roots.map(
1439
1782
  (r) => row(
1440
1783
  `root:${r}`,
1441
- tildify2(r),
1784
+ tildify3(r),
1442
1785
  "scanned for git repos",
1443
- () => ui.menu(tildify2(r), [
1786
+ () => ui.menu(tildify3(r), [
1444
1787
  { label: "Look for repos again", action: () => apply((w) => w.ops.rescan()) },
1445
- fixOption({ label: "Stop scanning this folder", preview: `Repos already found in ${tildify2(r)} stay listed until you hide them.`, run: (w) => w.ops.removeRoot(r) })
1788
+ fixOption({ label: "Stop scanning this folder", preview: `Repos already found in ${tildify3(r)} stay listed until you hide them.`, run: (w) => w.ops.removeRoot(r) })
1446
1789
  ])
1447
1790
  )
1448
1791
  ),
1449
1792
  row("root:add", "+ Add a folder\u2026", "", () => askForFolder("Folder that contains your projects")),
1450
- ...world.hidden.length ? [header("#hidden", "Hidden repos"), ...world.hidden.map((h) => row(`hidden:${h}`, tildify2(h), "enter shows it again", () => apply((w) => w.ops.unhide(h))))] : [],
1793
+ ...world.hidden.length ? [header("#hidden", "Hidden repos"), ...world.hidden.map((h) => row(`hidden:${h}`, tildify3(h), "enter shows it again", () => apply((w) => w.ops.unhide(h))))] : [],
1451
1794
  header("#backups", "Backups"),
1452
1795
  ...world.backups.length ? world.backups.map(
1453
1796
  (b, i) => row(`backup:${i}`, `\u21BA ${b.name}`, `${b.from} \xB7 ${b.at}`, () => apply((w) => w.ops.restoreBackup(i)))
@@ -1551,7 +1894,11 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1551
1894
  }
1552
1895
  if (m.kind === "input") {
1553
1896
  if (key.escape) return setModal(null);
1554
- if (key.return) return setModal(null), m.submit(m.value.trim());
1897
+ if (key.return) {
1898
+ const value = m.value.trim() || m.placeholder;
1899
+ setModal(null);
1900
+ return value ? m.submit(value) : void 0;
1901
+ }
1555
1902
  if (key.backspace || key.delete) return setModal({ ...m, value: m.value.slice(0, -1) });
1556
1903
  if (key.ctrl && input === "u") return setModal({ ...m, value: "" });
1557
1904
  if (input && !key.ctrl && !key.meta) return setModal({ ...m, value: m.value + input });
@@ -1564,7 +1911,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1564
1911
  if (key.return && m.options[m.cursor]) return setModal(null), choose(m.options[m.cursor]);
1565
1912
  return;
1566
1913
  }
1567
- if (key.escape) return setModal(null);
1914
+ if (key.escape) return setModal(null), m.kind === "confirm" && m.toast && setToast(m.toast);
1568
1915
  if (m.kind === "confirm") {
1569
1916
  if (key.return || input === "y") {
1570
1917
  setModal(null);
@@ -1601,17 +1948,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1601
1948
  if (key.return) {
1602
1949
  setModal(null);
1603
1950
  const chosen = m.items.filter((it) => it.on);
1604
- apply((w) => {
1605
- let ok = 0;
1606
- for (const it of chosen) {
1607
- try {
1608
- it.fix.run(w);
1609
- ok++;
1610
- } catch {
1611
- }
1612
- }
1613
- return `Applied ${ok} fix${ok === 1 ? "" : "es"} (anything removed is in Settings \u203A Backups)`;
1614
- });
1951
+ apply((w) => runAll(w, chosen.map((it) => it.fix)));
1615
1952
  }
1616
1953
  return;
1617
1954
  }
@@ -1634,9 +1971,9 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1634
1971
  const row = rows[cursor2];
1635
1972
  if (row.create && !/^[a-z0-9]/.test(m.query)) return setToast("Type the new skill's name first");
1636
1973
  setModal(null);
1637
- apply(row.run);
1638
- if (row.create) edit(world.ops.libraryFile(m.query));
1639
- return;
1974
+ if (!row.create) return apply(row.run);
1975
+ row.run(world);
1976
+ return edit(world.ops.libraryFile(m.query), row.afterEdit);
1640
1977
  }
1641
1978
  if (isNameChar(input, key)) return setModal({ ...m, query: m.query + input.toLowerCase(), cursor: 0 });
1642
1979
  }
@@ -1648,6 +1985,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1648
1985
  const globalFlagged = world.machine.map((m) => [m.name, machineIssues(world, m)]).filter(([, issues]) => issues.length);
1649
1986
  const globalIssues = new Set(globalFlagged.map(([name]) => name)).size;
1650
1987
  const globalProblem = globalFlagged.some(([, issues]) => issues.some((x) => x.severity === "problem"));
1988
+ const pluginsWorst = worst([...pluginIssues2.values()].flat().filter((i) => i.severity !== "hint"));
1651
1989
  const badge = (p) => {
1652
1990
  const found = projectIssues(world, p);
1653
1991
  const top = worst(found.map((x) => x.issue));
@@ -1669,6 +2007,9 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1669
2007
  sideRow("global", "\u25C8 Global", place === "global", () => goPlace("global"), [
1670
2008
  { text: String(new Set(world.machine.map((m) => m.name)).size), width: 4, color: globalProblem ? color.red : globalIssues ? color.yellow : color.muted }
1671
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
+ ]),
1672
2013
  sideRow("health", "\u2713 Health", place === "health", () => goPlace("health"), [
1673
2014
  { text: String(healthCount), width: 4, color: healthWorst ? SEV[healthWorst.severity].color : color.green }
1674
2015
  ]),
@@ -1706,17 +2047,17 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1706
2047
  }),
1707
2048
  /* @__PURE__ */ jsx2(Text2, { color: color.accent, children: query ? ` \u2315 ${query}\u258F` : "" })
1708
2049
  ] }) });
1709
- 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;
1710
2051
  const showChips = chips.length > 0;
1711
2052
  const listHeader = /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
1712
2053
  showChips ? chipRow : null,
1713
2054
  columnTitles ? /* @__PURE__ */ jsx2(TitleRow, { cells: columnTitles, width: columns }) : null
1714
2055
  ] });
1715
- const overviewH = dashboard && !sidebar ? 4 : 0;
2056
+ const overviewH = dashboard && !sidebar ? Math.ceil(overview(world, open).length / 2) + 2 : 0;
1716
2057
  const listH = split ? Math.floor((bodyH - overviewH) / 2) : bodyH - overviewH;
1717
2058
  const detailH = split ? bodyH - overviewH - listH : bodyH;
1718
2059
  const listRows = items.map((i) => ({ key: i.key, cells: i.cells, header: i.header }));
1719
- 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";
1720
2061
  const list = /* @__PURE__ */ jsx2(
1721
2062
  ListPanel,
1722
2063
  {
@@ -1749,8 +2090,11 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1749
2090
  );
1750
2091
  else if (modal?.kind === "input")
1751
2092
  body = /* @__PURE__ */ jsxs2(Panel, { title: modal.title, focused: true, width: columns, height: 5, children: [
1752
- /* @__PURE__ */ jsx2(Text2, { color: color.text, children: modal.value + "\u258F" }),
1753
- /* @__PURE__ */ jsx2(Text2, { color: color.faint, children: "enter to confirm \xB7 ctrl+u clears \xB7 esc cancels" })
2093
+ modal.value || !modal.placeholder ? /* @__PURE__ */ jsx2(Text2, { color: color.text, children: modal.value + "\u258F" }) : /* @__PURE__ */ jsxs2(Text2, { children: [
2094
+ "\u258F",
2095
+ /* @__PURE__ */ jsx2(Text2, { color: color.faint, children: modal.placeholder })
2096
+ ] }),
2097
+ /* @__PURE__ */ jsx2(Text2, { color: color.faint, children: modal.value || !modal.placeholder ? "enter to confirm \xB7 ctrl+u clears \xB7 esc cancels" : `type a path, or enter for ${modal.placeholder} \xB7 esc cancels` })
1754
2098
  ] });
1755
2099
  else if (modal?.kind === "agents")
1756
2100
  body = /* @__PURE__ */ jsx2(
@@ -1777,6 +2121,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1777
2121
  else if (modal?.kind === "repos") {
1778
2122
  const repos = (modal.only ?? world.projects.map((p) => p.name)).filter((r) => !modal.query || matches(r, modal.query));
1779
2123
  const name = current?.name ?? "";
2124
+ const usedHere = (r) => current?.plugin ? pluginUses(world, current.plugin, r) : world.usage[r]?.[name] ?? 0;
1780
2125
  const pickerLines = wrap(modal.fix.preview, columns - 6, 4);
1781
2126
  body = /* @__PURE__ */ jsx2(
1782
2127
  ListPanel,
@@ -1797,7 +2142,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1797
2142
  cells: [
1798
2143
  { text: modal.picked.has(r) ? "[x] " : "[ ] ", width: 4, color: modal.picked.has(r) ? color.green : color.faint },
1799
2144
  { text: r, width: 20 },
1800
- { 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 }
1801
2146
  ]
1802
2147
  }))
1803
2148
  }
@@ -1877,7 +2222,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1877
2222
  ] })
1878
2223
  ] }),
1879
2224
  /* @__PURE__ */ jsxs2(Box2, { height: 1, width: termCols, justifyContent: "space-between", children: [
1880
- /* @__PURE__ */ jsx2(Text2, { color: toast.startsWith("\u2713") || toast.startsWith("\u21BA") ? color.green : color.muted, wrap: "truncate-end", children: " " + toast }),
2225
+ /* @__PURE__ */ jsx2(Text2, { color: toast.startsWith("\u2713") || toast.startsWith("\u21BA") ? color.green : toast.startsWith("\u2717") ? color.red : toast.startsWith("!") ? color.yellow : color.muted, wrap: "truncate-end", children: " " + toast }),
1881
2226
  toast ? null : /* @__PURE__ */ jsx2(Text2, { color: color.faint, children: hints + " " })
1882
2227
  ] })
1883
2228
  ] });
@@ -1933,12 +2278,11 @@ function Help({ width, height }) {
1933
2278
  }
1934
2279
 
1935
2280
  // src/tui/load.ts
1936
- import { execFileSync, spawn, spawnSync as spawnSync3 } from "child_process";
2281
+ import { execFileSync, spawn } from "child_process";
1937
2282
  import { existsSync as existsSync3, lstatSync, realpathSync } from "fs";
1938
2283
  import { basename, dirname, join as join2 } from "path";
1939
- function tildify3(p) {
1940
- const home = userHome();
1941
- return (p.startsWith(home) ? "~" + p.slice(home.length) : p).replace(/\\/g, "/");
2284
+ function tildify4(p) {
2285
+ return tildify(p).replace(/\\/g, "/");
1942
2286
  }
1943
2287
  function uniqueNames(roots) {
1944
2288
  const tail = (r, n) => r.split(/[\\/]/).filter(Boolean).slice(-n).join("/");
@@ -1956,41 +2300,39 @@ function localSkill(root, s, git2) {
1956
2300
  "update available": {},
1957
2301
  "edited locally": { edited: true },
1958
2302
  "edited locally, update available": { edited: true },
1959
- "folder missing": { missing: true },
2303
+ // sync restores it from your library, unless the library doesn't have it (a teammate's skill, another library).
2304
+ "folder missing": { missing: true, ...s.latest === null && { library: "missing" } },
1960
2305
  "not in library": { library: "missing" }
1961
2306
  };
1962
2307
  const other = {
1963
2308
  "untracked copy of library skill": { source: "untracked", library: "same" },
1964
2309
  "untracked, differs from library": { source: "untracked", library: "differs" },
1965
2310
  "local only": { source: "untracked" },
2311
+ // Installed with `npx skills add` (the repo's skills-lock.json): not skilllib's, so not a library skill.
2312
+ "from npx skills": { source: "untracked" },
1966
2313
  "repo skill": { source: "repo" },
1967
2314
  "repo skill, in library": { source: "repo", library: "same" },
1968
2315
  "repo skill, differs from library": { source: "repo", library: "differs" }
1969
2316
  };
2317
+ const state = git2 ? git2.of(relativeTo(root, s.path)) : null;
2318
+ const own = other[s.state]?.source === "untracked" && (state === "committed" || state === "changed") ? { source: "repo" } : {};
2319
+ const blind = s.state === "folder missing" ? [] : s.visibility.filter((v) => v.paths === 0).map((v) => v.id);
2320
+ const uncommitted = blind.length ? linkEverywhere(root, s.name, s.location, { dryRun: true }).uncommitted : [];
2321
+ const unshared = blind.filter((id) => uncommitted.includes(harness(id).projectDirs[0]));
1970
2322
  return {
1971
2323
  name: s.name,
1972
2324
  source: "lib",
1973
2325
  dir: s.location,
1974
2326
  path: s.path,
1975
- agents: s.visibility.map((v) => v.id),
1976
- ...s.managed ? { library: "same", version: s.version ?? void 0, ...managed[s.state] } : other[s.state],
1977
- git: git2 ? git2.of(relativeTo(root, s.path)) : null
2327
+ // Agents that actually load it here: visibility lists every agent you use, with `paths: 0` for the blind ones.
2328
+ agents: s.visibility.filter((v) => v.paths > 0).map((v) => v.id),
2329
+ ...s.managed ? { library: "same", version: s.version ?? void 0, ...managed[s.state] } : { ...other[s.state], ...own },
2330
+ ...unshared.length && { unshared },
2331
+ ...s.source ? { origin: `npx skills: ${s.source}` } : {},
2332
+ added: addedAt(s.path),
2333
+ git: state
1978
2334
  };
1979
2335
  }
1980
- var partsOf = /* @__PURE__ */ new Map();
1981
- function pluginParts(root) {
1982
- if (!partsOf.has(root))
1983
- partsOf.set(
1984
- root,
1985
- [
1986
- ["commands", "commands"],
1987
- ["agents", "agents"],
1988
- ["hooks", "hooks"],
1989
- [".mcp.json", "MCP servers"]
1990
- ].flatMap(([file, what]) => existsSync3(join2(root, file)) ? [what] : [])
1991
- );
1992
- return partsOf.get(root);
1993
- }
1994
2336
  function installedAt(path) {
1995
2337
  try {
1996
2338
  const st = lstatSync(path);
@@ -2004,30 +2346,51 @@ function machineSkill(s, kept) {
2004
2346
  const global = s.kind === "global" || s.kind === "skills.sh";
2005
2347
  return {
2006
2348
  name: s.name,
2007
- source: global ? "global" : s.kind === "plugin" ? "plugin" : s.kind === "claude.ai" ? "claude.ai" : s.kind === "system" ? "system" : "cursor",
2008
- where: global ? tildify3(dirname(s.path)) : s.kind === "claude.ai" ? "claude.ai account" : s.kind === "built-in" ? `${s.origin} built-in` : s.origin,
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",
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,
2009
2351
  ...s.kind === "skills.sh" ? { origin: `npx skills: ${s.origin}` } : {},
2010
2352
  path: s.path,
2011
2353
  links: s.links,
2012
2354
  agents: s.harnesses,
2013
2355
  ...global && kept.has(s.name) ? { kept: true } : {},
2014
2356
  ...s.broken ? { broken: true } : {},
2015
- ...global && s.kind !== "skills.sh" ? { installed: installedAt(s.path) } : {},
2016
- ...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
2017
2377
  };
2018
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
+ }
2019
2382
  function dupesOf(r, root, git2 = null) {
2020
2383
  const out = /* @__PURE__ */ new Map();
2021
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)) });
2022
2385
  for (const c of r.conflicts)
2023
- out.set(c.name, { steps: [], git: [], differ: c.copies.map((x) => ({ dir: x.dir, label: root ? copyLabel(c, x.dir) : tildify3(x.dir), runs: x.runs, canWin: !c.managed || x.dir === c.managed })) });
2386
+ out.set(c.name, { steps: [], git: [], differ: c.copies.map((x) => ({ dir: x.dir, label: root ? copyLabel(c, x.dir) : tildify4(x.dir), runs: x.runs, canWin: !c.managed || x.dir === c.managed })) });
2024
2387
  return out;
2025
2388
  }
2026
2389
  function loadWorld() {
2027
2390
  forgetLatest();
2391
+ forgetClaudeBinary();
2028
2392
  const agents = enabledHarnesses();
2029
- const cwdRoot = findProjectRoot();
2030
- const here = isProjectCandidate(cwdRoot) && (existsSync3(join2(cwdRoot, ".git")) || existsSync3(join2(cwdRoot, "skilllib.json"))) ? cwdRoot : null;
2393
+ const here = projectHere();
2031
2394
  const real = (p) => {
2032
2395
  try {
2033
2396
  return realpathSync(p);
@@ -2046,7 +2409,8 @@ function loadWorld() {
2046
2409
  return { name: s.name, latest: latestVersion(s.name)?.version ?? 1, origin: origins[s.name] };
2047
2410
  });
2048
2411
  const kept = keptGlobal();
2049
- 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]));
2050
2414
  const globalDupes = dupesOf(planGlobalTidy({ enabled: agents }), null);
2051
2415
  const sourced = machineSkills(agents);
2052
2416
  const machine = sourced.map((s) => {
@@ -2057,7 +2421,7 @@ function loadWorld() {
2057
2421
  const plugin = cursorPlugin.get(m.name);
2058
2422
  return { ...m, ...dupes ? { dupes } : {}, ...plugin ? { cursorPlugin: plugin } : {} };
2059
2423
  });
2060
- const plugins = (root) => {
2424
+ const loadedHere = (root) => {
2061
2425
  const loaded = skillsLoadedIn(root, sourced, agents);
2062
2426
  if (loaded === sourced) return {};
2063
2427
  for (const s of loaded) descriptions[s.name] ??= s.description;
@@ -2078,22 +2442,26 @@ function loadWorld() {
2078
2442
  const plugin = cursorPlugin.get(s.name);
2079
2443
  return { ...localSkill(root, s, git2), ...d ? { dupes: d } : {}, ...plugin ? { cursorPlugin: plugin } : {} };
2080
2444
  }),
2081
- ...plugins(root)
2445
+ ...loadedHere(root)
2082
2446
  };
2083
2447
  });
2084
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));
2085
2451
  return {
2086
2452
  agents,
2087
2453
  cwd: cwd ? names.get(cwd) : null,
2088
2454
  projects,
2089
2455
  machine,
2456
+ plugins,
2090
2457
  library,
2091
2458
  usage: {},
2092
2459
  descriptions,
2093
- backups: backups.map((b) => ({ name: b.name, from: tildify3(b.from), at: b.movedAt })),
2460
+ backups: backups.map((b) => ({ name: b.name, from: tildify4(b.from), at: b.movedAt })),
2094
2461
  roots: readConfig().roots,
2095
2462
  hidden: readConfig().hidden,
2096
2463
  agentsChosen: harnessesChosen(),
2464
+ agentSkill: agentSkillState(agents),
2097
2465
  ops: realOps(new Map(projects.map((p) => [p.name, p.path])), backups)
2098
2466
  };
2099
2467
  }
@@ -2125,44 +2493,101 @@ function remoteLabel(url) {
2125
2493
  if (!url) return void 0;
2126
2494
  return url.replace(/^[a-z+]+:\/\//, "").replace(/^[^@/]+@/, "").replace(/:(?!\d)/, "/").replace(/\.git$/, "");
2127
2495
  }
2128
- function said2(c, repo) {
2496
+ function said3(c, repo) {
2129
2497
  if (c.action === "skipped") return `${c.name}: ${c.reason ?? "skipped"}`;
2130
2498
  if (c.action === "removed") return `Removed ${c.name} from ${repo}`;
2131
- return `${c.action === "installed" ? "Added" : "Updated"} ${c.name}${c.to ? ` v${c.to}` : ""} in ${repo}`;
2499
+ const back = c.action === "updated" && c.from !== void 0 && c.to !== void 0 && c.to < c.from;
2500
+ const done = back ? `Pinned ${c.name} to v${c.to} in ${repo} (was v${c.from})` : `${c.action === "installed" ? "Added" : c.action === "reset" ? "Reset" : "Updated"} ${c.name}${c.to ? ` v${c.to}` : ""} in ${repo}`;
2501
+ return c.backedUp ? `${done} (your edits are in Settings \u203A Backups)` : done;
2132
2502
  }
2133
2503
  function realOps(roots, backups) {
2134
2504
  const rootOf = (repo) => roots.get(repo);
2135
2505
  const find = (repo, name) => projectStatus(rootOf(repo)).find((s) => s.name === name);
2136
2506
  const info = /* @__PURE__ */ new Map();
2507
+ const result = (c, repo) => c.action === "skipped" ? failed(said3(c, repo)) : said3(c, repo);
2508
+ const copyDir = (repo, name) => readManifest(rootOf(repo)).skills[name]?.dir ?? PROJECT_SKILLS_DIR;
2137
2509
  const withLinks = (c, repo, name) => {
2138
- if (!c.blocked?.length) return said2(c, repo);
2139
- return { message: said2(c, repo), then: allowTracked(repo, name, c.blocked) };
2510
+ const it = (dirs) => dirs.length === 1 ? "it" : "them";
2511
+ if (c.uncommitted?.length)
2512
+ return `${said3(c, repo)}; not linked in ${c.uncommitted.join(", ")}: git tracks ${it(c.uncommitted)} and ${copyDir(repo, name)}/${name} isn't committed, so teammates would get broken links`;
2513
+ if (!c.blocked?.length) return result(c, repo);
2514
+ return { message: `${said3(c, repo)}; not linked in ${c.blocked.join(", ")}: git tracks ${it(c.blocked)}`, then: allowTracked([{ repo, name, blocked: c.blocked }]) };
2140
2515
  };
2141
- const allowTracked = (repo, name, blocked) => ({
2142
- label: `Also link it in ${blocked.join(", ")}`,
2143
- preview: `${blocked.join(", ")} is committed in ${repo}: the links will show in git status. skilllib remembers this for ${repo}.`,
2144
- run: (w) => {
2145
- allowTrackedLinks(rootOf(repo));
2146
- return w.ops.link(repo, name, true);
2147
- }
2148
- });
2149
- const orForce = (c, repo, label, force) => c.action === "skipped" && c.reason?.includes("edits") ? { message: said2(c, repo), then: { label, preview: `Your edits to ${c.name} in ${repo} are lost.`, run: () => said2(force(), repo) } } : said2(c, repo);
2516
+ const addTo = (repos, names) => {
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) => {
2521
+ const changes = repos.flatMap((repo) => names.map((name) => ({ repo, name, c: addSkill(rootOf(repo), name) })));
2522
+ const done = changes.filter((x) => x.c.action !== "skipped");
2523
+ const added = [...new Set(done.map((x) => x.name))];
2524
+ const summary = done.length ? `Added ${added.length === 1 ? `${added[0]} v${done[0].c.to}` : `${added.length} skills`} to ${[...new Set(done.map((x) => x.repo))].join(", ")}` : "";
2525
+ const notes = [];
2526
+ for (const repo of repos)
2527
+ for (const kind of ["blocked", "uncommitted"]) {
2528
+ const here = done.filter((x) => x.repo === repo && x.c[kind]?.length);
2529
+ if (!here.length) continue;
2530
+ const dirs = [...new Set(here.flatMap((x) => x.c[kind]))];
2531
+ const who = here.length === names.length ? "" : `${here.map((x) => x.name).join(", ")}: `;
2532
+ const copies = [...new Set(here.map((x) => copyDir(repo, x.name)))];
2533
+ const why = kind === "blocked" ? `git tracks ${dirs.length === 1 ? "it" : "them"}` : `${copies.join(", ")} ${copies.length === 1 ? "isn't" : "aren't"} committed, so a link would break for teammates`;
2534
+ notes.push(`${who}not linked in ${dirs.join(", ")} in ${repo}: ${why}`);
2535
+ }
2536
+ const skipped = changes.filter((x) => x.c.action === "skipped").map((x) => `${x.name} not added to ${x.repo}: ${x.c.reason}`);
2537
+ const text = [summary, ...notes, ...skipped].filter(Boolean).join("; ");
2538
+ const held = done.flatMap((x) => x.c.blocked?.length ? [{ repo: x.repo, name: x.name, blocked: x.c.blocked }] : []);
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 };
2541
+ };
2542
+ const allowTracked = (held) => {
2543
+ const repos = [...new Set(held.map((h) => h.repo))];
2544
+ const dirs = [...new Set(held.flatMap((h) => h.blocked))];
2545
+ return {
2546
+ label: `Also link ${new Set(held.map((h) => h.name)).size === 1 ? "it" : "them"} in ${dirs.join(", ")}`,
2547
+ preview: `${dirs.join(", ")} ${dirs.length === 1 ? "is" : "are"} committed in ${repos.join(", ")}: the links will show in git status. skilllib remembers this for ${repos.join(", ")}.`,
2548
+ run: (w) => {
2549
+ for (const repo of repos) allowTrackedLinks(rootOf(repo));
2550
+ if (held.length === 1) return w.ops.link(held[0].repo, held[0].name, true);
2551
+ const results = held.map((h) => ({ ...h, r: linkIn(h.repo, h.name, true) }));
2552
+ const linked = results.filter((x) => x.r.created.length);
2553
+ const names = [...new Set(linked.map((x) => x.name))];
2554
+ const summary = linked.length ? `Linked ${names.length === 1 ? names[0] : `${names.length} skills`} in ${[...new Set(linked.flatMap((x) => x.r.created))].join(", ")} in ${[...new Set(linked.map((x) => x.repo))].join(", ")}` : "";
2555
+ const others = results.filter((x) => !x.r.created.length || x.r.uncommitted.length).map((x) => `${x.repo}: ${linkSaid(x.name, x.r)}`);
2556
+ return [summary, ...others].join("; ");
2557
+ }
2558
+ };
2559
+ };
2560
+ const linkIn = (repo, name, allow) => {
2561
+ const s = find(repo, name);
2562
+ return s.managed ? relinkDependency(rootOf(repo), name, { allowTracked: allow }) : linkEverywhere(rootOf(repo), name, s.location, { allowTracked: allow });
2563
+ };
2564
+ const linkSaid = (name, r) => {
2565
+ const linked = r.created.length ? `linked in ${r.created.join(", ")}` : "";
2566
+ const notLinked = r.uncommitted.length ? `not linked in ${r.uncommitted.join(", ")}: git tracks ${r.uncommitted.length === 1 ? "it" : "them"} and ${name} isn't committed, so teammates would get broken links` : "";
2567
+ return `${name}: ${[linked, notLinked].filter(Boolean).join("; ") || "nothing to link"}`;
2568
+ };
2569
+ const orForce = (c, repo, label, force) => c.action === "skipped" && c.reason?.includes("edits") ? {
2570
+ message: `${c.name} in ${repo} has local edits`,
2571
+ then: { label, preview: `Your edits to ${c.name} in ${repo} go to Settings \u203A Backups.`, run: () => result(force(), repo) }
2572
+ } : result(c, repo);
2150
2573
  const toLibrary = (path) => {
2151
2574
  const r = importSkill(path, { force: true });
2152
2575
  return r.status === "unchanged" ? `${r.name} is already the same in your library` : `${r.name} v${latestVersion(r.name)?.version} is in your library`;
2153
2576
  };
2154
2577
  return {
2155
- add: (repo, name) => withLinks(addSkill(rootOf(repo), name), repo, name),
2578
+ add: (repo, name) => addTo([repo], [name]),
2579
+ addTo,
2156
2580
  remove: (repo, name) => {
2157
2581
  const s = find(repo, name);
2158
- if (!s) return `${name} isn't in ${repo}`;
2159
- if (s.state.startsWith("repo skill")) return `${name} is the repo's own (committed by your team): skilllib doesn't delete it`;
2582
+ if (!s) return failed(`${name} isn't in ${repo}`);
2583
+ const committed = !s.managed && ["committed", "changed"].includes(gitInfo(rootOf(repo))?.of(relativeTo(rootOf(repo), s.path)) ?? "");
2584
+ if (s.state.startsWith("repo skill") || committed) return failed(`${name} is the repo's own (committed by your team): skilllib doesn't delete it`);
2160
2585
  if (!s.managed) return removeUntracked(rootOf(repo), name, s.path), `Removed ${name} from ${repo} (in Settings \u203A Backups)`;
2161
2586
  return orForce(removeSkill(rootOf(repo), name), repo, "Remove it anyway", () => removeSkill(rootOf(repo), name, { force: true }));
2162
2587
  },
2163
2588
  restore: (repo, name) => {
2164
2589
  const c = syncProject(rootOf(repo)).find((x) => x.name === name);
2165
- return c ? said2(c, repo) : `${name} is already in place`;
2590
+ return c ? result(c, repo) : `${name} is already in place`;
2166
2591
  },
2167
2592
  update: (repo, name) => {
2168
2593
  const c = updateProject(rootOf(repo), [name])[0];
@@ -2180,10 +2605,10 @@ function realOps(roots, backups) {
2180
2605
  return withLinks(addSkill(rootOf(repo), name, s.managed && s.version ? { version: s.version } : {}), repo, name);
2181
2606
  },
2182
2607
  link: (repo, name, allow = false) => {
2183
- const s = find(repo, name);
2184
- const r = s.managed ? relinkDependency(rootOf(repo), name, { allowTracked: allow }) : linkEverywhere(rootOf(repo), name, s.location, { allowTracked: allow });
2185
- const message = r.created.length ? `${name}: linked in ${r.created.join(", ")}` : `${name}: nothing to link`;
2186
- return r.blocked.length && !allow ? { message, then: allowTracked(repo, name, r.blocked) } : message;
2608
+ const r = linkIn(repo, name, allow);
2609
+ const message = linkSaid(name, r);
2610
+ if (!r.blocked.length || allow) return r.created.length ? message : failed(message);
2611
+ return { message: `${message}; not linked in ${r.blocked.join(", ")}: git tracks ${r.blocked.length === 1 ? "it" : "them"}`, then: allowTracked([{ repo, name, blocked: r.blocked }]) };
2187
2612
  },
2188
2613
  track: (repo, name) => withLinks(addSkill(rootOf(repo), name), repo, name),
2189
2614
  importLocal: (repo, name) => {
@@ -2199,10 +2624,9 @@ function realOps(roots, backups) {
2199
2624
  if (!plan) return `${name}: nothing to tidy`;
2200
2625
  const r = applyTidy(plan, { git: allowGit ? "go" : "keep" });
2201
2626
  const steps = (list) => list.map((s) => describeStep(s, root)).join(", ");
2202
- const message = r.applied.length ? `${name}: ${steps(r.applied)}` : `${name}: nothing changed yet`;
2203
- if (!r.held.length) return message;
2627
+ if (!r.held.length) return r.applied.length ? `${name}: ${steps(r.applied)}` : `${name}: nothing changed`;
2204
2628
  return {
2205
- message,
2629
+ message: `${name}: ${r.applied.length ? `${steps(r.applied)}; ` : ""}held back ${steps(r.held)}, because git tracks it`,
2206
2630
  then: {
2207
2631
  label: "Also change what git tracks",
2208
2632
  preview: `${steps(r.held)}: these changes will show in git status in ${repo}. Replaced copies go to Settings \u203A Backups.`,
@@ -2213,70 +2637,118 @@ function realOps(roots, backups) {
2213
2637
  unloadGlobal: (m) => {
2214
2638
  if (!m.broken && !latestVersion(m.name)) importSkill(m.path);
2215
2639
  const r = unloadGlobal(m.path, m.links);
2216
- return r.ok ? `${m.name} no longer loads globally (in Settings \u203A Backups)` : `${m.name}: ${r.reason}`;
2640
+ return r.ok ? `${m.name} no longer loads globally (in Settings \u203A Backups)` : failed(`${m.name}: ${r.reason}`);
2217
2641
  },
2218
2642
  deleteGlobal: (m) => {
2219
2643
  const r = deleteGlobal(m.path, m.links);
2220
- return r.ok ? m.broken ? `Removed the broken link ${m.name}` : `Deleted ${m.name} (in Settings \u203A Backups)` : `${m.name}: ${r.reason}`;
2644
+ return r.ok ? m.broken ? `Removed the broken link ${m.name}` : `Deleted ${m.name} (in Settings \u203A Backups)` : failed(`${m.name}: ${r.reason}`);
2221
2645
  },
2222
2646
  keepGlobal: (name, keep) => (setKeepGlobal([name], keep), keep ? `${name} marked as global on purpose` : `${name} is no longer marked as global on purpose`),
2223
2647
  linkGlobal: (m, agents) => {
2224
2648
  const r = linkGlobal(m.path, agents);
2225
- const linked = r.linked.map(tildify3).join(", ");
2226
- const skipped = r.skipped.length ? `; ${r.skipped.map(tildify3).join(", ")} already holds a different skill` : "";
2227
- return `${m.name}: ${linked ? `linked ${linked}` : "nothing linked"}${skipped}`;
2649
+ const linked = r.linked.map(tildify4).join(", ");
2650
+ const skipped = r.skipped.length ? `; ${r.skipped.map(tildify4).join(", ")} already holds a different skill` : "";
2651
+ const message = `${m.name}: ${linked ? `linked ${linked}` : "nothing linked"}${skipped}`;
2652
+ return !linked && skipped ? failed(message) : message;
2228
2653
  },
2229
- moveGlobal: (m, repos, group) => {
2230
- if (importSkill(m.path).status === "exists") return `${m.name}: your library has a different ${m.name}; nothing moved (update your library from this copy first)`;
2231
- if (group) recordOrigin(m.name, `group: ${group}`);
2232
- const results = repos.map((r2) => [r2, addSkill(rootOf(r2), m.name)]);
2233
- const skipped = results.filter(([, c]) => c.action === "skipped");
2234
- if (skipped.length)
2235
- return `${m.name}: not added to ${skipped.map(([r2, c]) => `${r2} (${c.reason})`).join(", ")}, so it still loads globally`;
2236
- const blocked = results.filter(([, c]) => c.blocked?.length).map(([r2]) => r2);
2237
- const r = unloadGlobal(m.path, m.links);
2238
- if (!r.ok) return `${m.name}: ${r.reason}`;
2239
- return `${m.name} now loads only in ${repos.join(", ")}${blocked.length ? ` (some agents can't see it in ${blocked.join(", ")}: git tracks the folder)` : ""}`;
2654
+ moveGlobal: (skills, repos, group) => {
2655
+ const moved = [];
2656
+ const notes = [];
2657
+ const held = [];
2658
+ for (const m of skills) {
2659
+ if (importSkill(m.path).status === "exists") {
2660
+ notes.push(`${m.name}: your library has a different ${m.name}; nothing moved (update your library from this copy first)`);
2661
+ continue;
2662
+ }
2663
+ if (group) recordOrigin(m.name, `group: ${group}`);
2664
+ const results = repos.map((r2) => [r2, addSkill(rootOf(r2), m.name)]);
2665
+ const skipped = results.filter(([, c]) => c.action === "skipped");
2666
+ if (skipped.length) {
2667
+ notes.push(`${m.name}: not added to ${skipped.map(([r2, c]) => `${r2} (${c.reason})`).join(", ")}, so it still loads globally`);
2668
+ continue;
2669
+ }
2670
+ const r = unloadGlobal(m.path, m.links);
2671
+ if (!r.ok) {
2672
+ notes.push(`${m.name}: ${r.reason}`);
2673
+ continue;
2674
+ }
2675
+ moved.push(m.name);
2676
+ for (const [repo, c] of results) {
2677
+ const blind = visibilityOf(rootOf(repo), m.name).flatMap((v) => v.paths === 0 ? [v.id] : []);
2678
+ const blocked = [...c.blocked ?? [], ...c.uncommitted ?? []];
2679
+ if (!blocked.length || !blind.length) continue;
2680
+ held.push({ repo, name: m.name, blocked, blind, copy: copyDir(repo, m.name), shared: !c.uncommitted?.length });
2681
+ }
2682
+ }
2683
+ const gaps = /* @__PURE__ */ new Map();
2684
+ for (const repo of new Set(held.map((h) => h.repo)))
2685
+ for (const shared of [true, false]) {
2686
+ const here = held.filter((h) => h.repo === repo && h.shared === shared);
2687
+ if (!here.length) continue;
2688
+ const agents = [...new Set(here.flatMap((h) => h.blind))].map((id) => harness(id).name).join(", ");
2689
+ const what = skills.length === 1 ? "it" : here.length === moved.length ? "them" : here.map((h) => h.name).join(", ");
2690
+ const dirs = [...new Set(here.flatMap((h) => h.blocked))].join(", ");
2691
+ const why = shared ? `git tracks ${dirs}` : `${[...new Set(here.map((h) => h.copy))].join(", ")} isn't committed, so a link in ${dirs} would break for teammates`;
2692
+ const key = `${agents}|${what}|${why}`;
2693
+ if (!gaps.has(key)) gaps.set(key, { who: `${agents} can't see ${what}`, why, repos: [] });
2694
+ gaps.get(key).repos.push(repo);
2695
+ }
2696
+ const blindNotes = [...gaps.values()].map((g) => `${g.who} in ${g.repos.join(", ")}: ${g.why}`);
2697
+ const summary = moved.length ? `${moved.length === 1 ? moved[0] : `${moved.length} skills`} now load${moved.length === 1 ? "s" : ""} only in ${repos.join(", ")}` : "";
2698
+ const text = [summary, ...blindNotes, ...notes].filter(Boolean).join("; ") || "Nothing moved";
2699
+ const askable = held.filter((h) => h.shared);
2700
+ if (!moved.length && notes.length) return failed(text);
2701
+ return askable.length ? { message: text, then: allowTracked(askable) } : text;
2240
2702
  },
2241
2703
  createSkill: (name) => {
2242
2704
  const r = createSkill(name, "");
2243
- return r.ok ? `Created ${name} in your library: edit its SKILL.md from Your skills` : `${name}: ${r.reason}`;
2705
+ return r.ok ? `Created ${name} in your library: edit its SKILL.md from Your skills` : failed(`${name}: ${r.reason}`);
2244
2706
  },
2245
2707
  pluginOffHere: (repo, id) => {
2246
2708
  const root = rootOf(repo);
2247
2709
  const plugin = claudePlugins(skillsLoadedIn(root, machineSkills()), root).find((p) => p.id === id);
2248
- if (!plugin) return `${id} isn't on in ${repo}`;
2710
+ if (!plugin) return failed(`${id} isn't on in ${repo}`);
2249
2711
  const r = turnOffIn(plugin, root);
2250
- if (!r.ok) throw new Error(r.message);
2251
- return r.message;
2712
+ return r.ok ? r.message : failed(r.message);
2252
2713
  },
2253
- replacePlugin: (id, repos) => {
2254
- const skills = machineSkills().filter((s) => s.kind === "plugin" && s.origin === id && !s.broken);
2255
- const clashes = skills.filter((s) => importSkill(s.path).status === "exists").map((s) => s.name);
2256
- if (clashes.length) return `${id} kept: your library has different ${clashes.join(", ")}; update or delete ${clashes.length === 1 ? "it" : "them"} first`;
2257
- const skipped = repos.flatMap((r) => skills.flatMap((s) => {
2258
- const c = addSkill(rootOf(r), s.name);
2259
- return c.action === "skipped" ? [`${s.name} in ${r} (${c.reason})`] : [];
2260
- }));
2261
- if (skipped.length) return `${id} kept: not added ${skipped.join(", ")}`;
2262
- const copied = `${skills.length} skill${skills.length === 1 ? " from " + id + " is" : "s from " + id + " are"} in your library${repos.length ? ` and in ${repos.join(", ")}` : ""}`;
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);
2718
+ if (clashes.length) return failed(`${id} kept: your library has different ${clashes.join(", ")}; update or delete ${clashes.length === 1 ? "it" : "them"} first`);
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);
2263
2728
  const bin = claudeBinary();
2264
- const claude = (...args) => spawnSync3(bin, ["plugin", ...args], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: 2e4 });
2265
- const synced = id.endsWith("@synced");
2266
- const off = bin && !synced && claude("uninstall", id, "--keep-data").status === 0 ? "uninstalled" : bin ? claude("disable", id) : null;
2267
- if (off === "uninstalled") {
2268
- recordRemovedPlugin(id, "user");
2269
- return `${copied}; ${id} is uninstalled (restore it from Settings \u203A Backups, or reinstall 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}: `, "")})`;
2270
2734
  }
2271
- if (off?.status === 0)
2272
- 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}`;
2273
- 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}`;
2274
- try {
2275
- setPluginEnabled(id, false);
2276
- } catch (e) {
2277
- return `${copied}; couldn't turn ${id} off (${why}; ${e.message}): turn it off with /plugin`;
2278
- }
2279
- 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);
2280
2752
  },
2281
2753
  deleteLibrary: (name) => {
2282
2754
  const from = [];
@@ -2287,13 +2759,13 @@ function realOps(roots, backups) {
2287
2759
  from.push([...roots].find(([, r2]) => r2 === root)?.[0] ?? basename(root));
2288
2760
  }
2289
2761
  const r = deleteLibrarySkill(name);
2290
- if (!r.ok) return `${name}: ${r.reason}`;
2762
+ if (!r.ok) return failed(`${name}: ${r.reason}`);
2291
2763
  return `Deleted ${name} from your library${from.length ? ` and from ${from.join(", ")}` : ""} (in Settings \u203A Backups)`;
2292
2764
  },
2293
2765
  restoreBackup: (i) => {
2294
2766
  const b = backups[i];
2295
2767
  const r = b.kind === "plugin" ? restorePlugin(b) : restoreBackup(b);
2296
- return r.ok ? `Restored ${b.name} to ${tildify3(r.to)}` : `${b.name}: ${r.reason}`;
2768
+ return r.ok ? `Restored ${b.name} to ${tildify4(r.to)}` : failed(`${b.name}: ${r.reason}`);
2297
2769
  },
2298
2770
  setAgents: (ids) => (setHarnesses(ids), "Saved your agents"),
2299
2771
  installVersion: (repo, name, version) => withLinks(addSkill(rootOf(repo), name, { version, force: true }), repo, name),
@@ -2302,20 +2774,28 @@ function realOps(roots, backups) {
2302
2774
  addRoot: (path) => {
2303
2775
  const root = addRoot(path);
2304
2776
  discoverProjects();
2305
- return `Scanning ${tildify3(root)} for repos`;
2777
+ return `Scanning ${tildify4(root)} for repos`;
2306
2778
  },
2307
- removeRoot: (path) => (removeRoot(path), `Stopped scanning ${tildify3(path)}; repos already found stay until you hide them`),
2779
+ removeRoot: (path) => (removeRoot(path), `Stopped scanning ${tildify4(path)}; repos already found stay until you hide them`),
2308
2780
  rescan: () => (discoverProjects(), "Looked for repos in your project folders again"),
2309
2781
  hide: (repo) => (setHidden(rootOf(repo), true), `Hid ${repo}: Settings \u203A Hidden repos brings it back`),
2310
- unhide: (path) => (setHidden(path, false), `${tildify3(path)} is back in the list`),
2782
+ unhide: (path) => (setHidden(path, false), `${tildify4(path)} is back in the list`),
2311
2783
  openFolder: (path) => {
2312
2784
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
2313
2785
  spawn(cmd, [path], { detached: true, stdio: "ignore" }).unref();
2314
- return `Opened ${tildify3(path)}`;
2786
+ return `Opened ${tildify4(path)}`;
2315
2787
  },
2316
2788
  copy: (text, what) => {
2317
2789
  const r = copyText(text);
2318
- return r.copied ? `Copied ${what}: paste it into Claude Code, Cursor or Codex` : `Couldn't reach the clipboard; ${what} is in ${tildify3(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)}`;
2791
+ },
2792
+ installAgentSkill: () => {
2793
+ const r = installAgentSkill();
2794
+ return r.ok ? `Your agents can now use skilllib (${r.dirs.map(tildify4).join(", ")})` : failed(`skilllib skill: ${r.reason}`);
2795
+ },
2796
+ removeAgentSkill: () => {
2797
+ const removed = removeAgentSkill();
2798
+ return removed.length ? `Removed the skilllib skill (${removed.map(tildify4).join(", ")})` : failed("The skilllib skill isn't installed");
2319
2799
  },
2320
2800
  repoInfo: (repo) => {
2321
2801
  if (!info.has(repo)) {