skilllib 1.6.0 → 1.7.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,
@@ -14,14 +17,13 @@ import {
14
17
  describeStep,
15
18
  discoverProjects,
16
19
  enabledHarnesses,
17
- findProjectRoot,
18
20
  forgetLatest,
19
21
  gitInfo,
20
22
  gitVisibleSteps,
21
23
  harness,
22
24
  harnessesChosen,
23
25
  importSkill,
24
- isProjectCandidate,
26
+ installAgentSkill,
25
27
  keptGlobal,
26
28
  latestVersion,
27
29
  libraryOrigins,
@@ -34,15 +36,18 @@ import {
34
36
  planGlobalTidy,
35
37
  planProjectTidy,
36
38
  pluginBackups,
39
+ projectHere,
37
40
  projectOfFactory,
38
41
  projectStatus,
39
42
  projectsUsing,
40
43
  readConfig,
44
+ readManifest,
41
45
  readSkillInfo,
42
46
  recordOrigin,
43
47
  recordRemovedPlugin,
44
48
  relativeTo,
45
49
  relinkDependency,
50
+ removeAgentSkill,
46
51
  removeRoot,
47
52
  removeSkill,
48
53
  removeUntracked,
@@ -56,13 +61,14 @@ import {
56
61
  skilllibHome,
57
62
  skillsLoadedIn,
58
63
  syncProject,
64
+ tildify,
59
65
  turnOffIn,
60
66
  unloadGlobal,
61
67
  updateProject,
62
- userHome,
63
68
  versionHistory,
69
+ visibilityOf,
64
70
  visibleProjects
65
- } from "./chunk-PHRUPZGX.js";
71
+ } from "./chunk-AYAN7Q2X.js";
66
72
 
67
73
  // src/tui/index.tsx
68
74
  import { render } from "ink";
@@ -188,10 +194,7 @@ import { spawnSync } from "child_process";
188
194
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
189
195
  import { join } from "path";
190
196
  var EXCERPT_CHARS = 1200;
191
- function tildify(p) {
192
- const home = userHome();
193
- return p.startsWith(home) ? "~" + p.slice(home.length) : p;
194
- }
197
+ var tildify2 = (p) => tildify(p);
195
198
  function skillExcerpt(dir, chars) {
196
199
  const file = join(dir, "SKILL.md");
197
200
  if (!existsSync(file)) return { text: "(SKILL.md not found)", files: 0, size: 0 };
@@ -215,7 +218,7 @@ function buildReviewPrompt(skills, ctx) {
215
218
  `- Description: ${s.description || "(none)"}`,
216
219
  `- Source: ${s.source}`,
217
220
  `- Installed via: ${s.installedHow}`,
218
- `- Location: ${tildify(s.path)}${s.links.length ? ` (also linked from ${s.links.map(tildify).join(", ")})` : ""}`,
221
+ `- Location: ${tildify2(s.path)}${s.links.length ? ` (also linked from ${s.links.map(tildify2).join(", ")})` : ""}`,
219
222
  `- Loaded by: ${s.loadedBy}`,
220
223
  ...s.vendor ? [`- Vendor-managed: ${s.vendor}`] : [],
221
224
  ...s.keptGlobal ? ["- I marked this as global on purpose: recommend changing that only for a strong reason (unused, duplicated, low quality)"] : [],
@@ -241,7 +244,7 @@ function buildReviewPrompt(skills, ctx) {
241
244
  - 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
245
 
243
246
  ## My projects
244
- ${ctx.projects.map((p) => `- ${p.name} \u2014 ${tildify(p.path)}`).join("\n") || "(none)"}
247
+ ${ctx.projects.map((p) => `- ${p.name} \u2014 ${tildify2(p.path)}`).join("\n") || "(none)"}
245
248
 
246
249
  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
250
 
@@ -292,9 +295,17 @@ function matchScore(text, query) {
292
295
  }
293
296
 
294
297
  // src/tui/world.ts
298
+ var failed = (message) => ({ message, failed: true });
299
+ var isFailure = (r) => typeof r !== "string" && "failed" in r;
300
+ var followUp = (r) => typeof r !== "string" && "then" in r ? r.then : void 0;
301
+ var said = (r) => typeof r === "string" ? r : r.message;
302
+ var joined = (results) => {
303
+ const message = results.map(said).join(" \xB7 ");
304
+ return results.some(isFailure) ? failed(message) : message;
305
+ };
295
306
  var SEVERITY_RANK = { problem: 0, warning: 1, hint: 2 };
296
307
  function filterOf(source) {
297
- return source === "lib" || source === "repo" || source === "untracked" ? "local" : source === "global" ? "global" : source === "plugin" ? "plugins" : "vendor";
308
+ return source === "lib" || source === "repo" || source === "untracked" ? "local" : source === "global" || source === "skilllib" ? "global" : source === "plugin" ? "plugins" : "vendor";
298
309
  }
299
310
  function project(w, name) {
300
311
  return w.projects.find((p) => p.name === name);
@@ -325,6 +336,10 @@ function lib(w, name) {
325
336
  return w.library.find((l) => l.name === name);
326
337
  }
327
338
  var agentNames = (ids) => ids.map((a) => harness(a).name).join(", ");
339
+ function usedElsewhere(w, except, name) {
340
+ 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);
341
+ return repos.length ? `, and ${repos.join(", ")} used it in the last 30 days` : "";
342
+ }
328
343
  function dupeIssues(repo, name, d) {
329
344
  const g = repo === null ? "-g" : "";
330
345
  if (d.differ)
@@ -381,7 +396,7 @@ function globalFixes(m) {
381
396
  label: "Move it to the repos that need it\u2026",
382
397
  preview: `Pick repos. ${m.name} goes into your library and those repos, and stops loading globally (original backed up).`,
383
398
  run: () => "",
384
- pickRepos: (w, repos) => w.ops.moveGlobal(m, repos)
399
+ pickRepos: (w, repos) => w.ops.moveGlobal([m], repos)
385
400
  },
386
401
  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
402
  { label: "Delete it", preview: `Remove ${m.path}. A backup is kept in Settings \u203A Backups.`, run: (w) => w.ops.deleteGlobal(m) }
@@ -389,9 +404,10 @@ function globalFixes(m) {
389
404
  }
390
405
  function machineActions(m) {
391
406
  if (m.broken) return [];
392
- return m.source === "global" ? globalFixes(m) : [];
407
+ return m.source === "global" ? globalFixes(m) : m.source === "skilllib" ? [removeAgentSkillFix()] : [];
393
408
  }
394
409
  function machineIssues(w, m) {
410
+ if (m.source === "skilllib") return [agentSkillIssue(w)].flatMap((i) => i ? [i] : []);
395
411
  if (m.broken)
396
412
  return [
397
413
  {
@@ -411,11 +427,12 @@ function machineIssues(w, m) {
411
427
  severity: "problem",
412
428
  title: `Loaded twice: also in ${other.source} ${other.where}`,
413
429
  short: "Loaded twice",
414
- decision: false,
430
+ // Every repo then runs the other copy instead of yours: a decision, never "fix all".
431
+ decision: true,
415
432
  fixes: [
416
433
  {
417
434
  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.`,
435
+ 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
436
  run: (w2) => w2.ops.unloadGlobal(m)
420
437
  }
421
438
  ]
@@ -458,7 +475,16 @@ function issuesOf(w, projectName, u) {
458
475
  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
476
  run: (w2) => w2.ops.remove(projectName, s.name)
460
477
  };
461
- if (s.missing)
478
+ if (s.missing && s.library === "missing")
479
+ issues.push({
480
+ id: `unavailable:${s.name}`,
481
+ severity: "problem",
482
+ title: "Pinned in skilllib.json, but not in your library: import it, or set SKILLLIB_HOME to the library that has it",
483
+ short: "Pinned, not in your library",
484
+ decision: true,
485
+ 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) }]
486
+ });
487
+ else if (s.missing)
462
488
  issues.push({
463
489
  id: `missing:${s.name}`,
464
490
  severity: "problem",
@@ -467,7 +493,7 @@ function issuesOf(w, projectName, u) {
467
493
  decision: false,
468
494
  fixes: [{ label: "Restore it (sync)", preview: `Reinstall ${s.name} v${s.version} into ${s.dir}.`, run: (w2) => w2.ops.restore(projectName, s.name) }]
469
495
  });
470
- if (s.source === "lib" && s.library === "missing")
496
+ else if (s.source === "lib" && s.library === "missing")
471
497
  issues.push({
472
498
  id: `not-in-lib:${s.name}`,
473
499
  severity: "problem",
@@ -487,7 +513,7 @@ function issuesOf(w, projectName, u) {
487
513
  ...s.source !== "repo" ? [{ ...remove, label: "Remove this repo's copy, keep it global" }] : [],
488
514
  {
489
515
  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.`,
516
+ 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
517
  run: (w2) => w2.ops.unloadGlobal(g)
492
518
  }
493
519
  ]
@@ -498,17 +524,28 @@ function issuesOf(w, projectName, u) {
498
524
  severity: "problem",
499
525
  title: `Loaded twice: also global in ${g.where}`,
500
526
  short: "Loaded twice",
501
- decision: false,
527
+ // Unloading the global copy changes every repo: a decision, never "fix all".
528
+ decision: true,
502
529
  fixes: [
503
530
  {
504
531
  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.`,
532
+ 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
533
  run: (w2) => w2.ops.unloadGlobal(g)
507
534
  },
508
535
  ...s.source !== "repo" ? [{ ...remove, label: "Keep it global, remove it from this repo" }] : []
509
536
  ]
510
537
  });
511
- if (vendor)
538
+ const cursorOnly = vendor?.source === "plugin" && w.agents.includes("claude-code") && !vendor.agents.includes("claude-code");
539
+ if (vendor && cursorOnly)
540
+ issues.push({
541
+ id: `twice-p:${s.name}`,
542
+ severity: "warning",
543
+ title: `Same name as a skill in plugin ${vendor.where}: Claude Code has it off in ${projectName}, but Cursor ignores repo settings and lists both`,
544
+ short: "Cursor also lists a plugin's copy",
545
+ decision: true,
546
+ fixes: [replacePluginFix(w, vendor.where)]
547
+ });
548
+ else if (vendor)
512
549
  issues.push({
513
550
  id: `twice-p:${s.name}`,
514
551
  severity: "problem",
@@ -519,7 +556,7 @@ function issuesOf(w, projectName, u) {
519
556
  ...s.source !== "repo" ? [{ ...remove, label: `Remove this repo's copy, use the ${vendor.source}'s` }] : [],
520
557
  vendor.source === "plugin" ? {
521
558
  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.`,
559
+ 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
560
  run: (w2) => w2.ops.pluginOffHere(projectName, vendor.where)
524
561
  } : {
525
562
  label: `Turn it off in ${vendor.where}`,
@@ -552,21 +589,34 @@ function issuesOf(w, projectName, u) {
552
589
  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
590
  });
554
591
  const blind = w.agents.filter((a) => !u.agents.includes(a));
555
- if (!s.missing && blind.length)
592
+ const unshared = blind.filter((a) => s.unshared?.includes(a));
593
+ const linkable = blind.filter((a) => !unshared.includes(a));
594
+ const commitFirst = unshared.length ? ` ${agentNames(unshared)} ${unshared.length === 1 ? "needs" : "need"} it committed first (git tracks the folder they read).` : "";
595
+ if (!s.missing && linkable.length)
556
596
  issues.push({
557
597
  id: `blind:${s.name}`,
558
598
  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`,
599
+ title: `${agentNames(blind)} can't see it`,
600
+ short: `${agentNames(blind)} can't see it`,
561
601
  decision: false,
562
602
  fixes: [
563
603
  {
564
604
  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.`,
605
+ preview: `Add links (nothing is copied or moved), so ${agentNames(linkable)} ${linkable.length === 1 ? "loads" : "load"} it too.${commitFirst}`,
566
606
  run: (w2) => w2.ops.link(projectName, s.name)
567
607
  }
568
608
  ]
569
609
  });
610
+ else if (!s.missing && unshared.length)
611
+ issues.push({
612
+ id: `blind:${s.name}`,
613
+ severity: "warning",
614
+ 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)`,
615
+ short: `${agentNames(unshared)} can't see it: commit it first`,
616
+ // Nothing skilllib can do: committing is yours.
617
+ decision: true,
618
+ fixes: []
619
+ });
570
620
  const differ = !!s.dupes?.differ;
571
621
  if (s.source !== "lib" && s.library === "differs" && !differ)
572
622
  issues.push({
@@ -586,7 +636,8 @@ function issuesOf(w, projectName, u) {
586
636
  severity: "hint",
587
637
  title: "Same as your library skill, but not tracked",
588
638
  short: "Not tracked",
589
- decision: false,
639
+ // It writes to the repo's skilllib.json (and your library): your call, never in Fix all (as in doctor, #11).
640
+ decision: true,
590
641
  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
642
  });
592
643
  if (s.source === "untracked" && !l && !vendor && !differ)
@@ -595,7 +646,8 @@ function issuesOf(w, projectName, u) {
595
646
  severity: "hint",
596
647
  title: "Only exists in this repo",
597
648
  short: "Only in this repo",
598
- decision: false,
649
+ // It writes to the repo's skilllib.json (and your library): your call, never in Fix all (as in doctor, #11).
650
+ decision: true,
599
651
  fixes: [
600
652
  {
601
653
  label: "Import it into your library",
@@ -604,7 +656,7 @@ function issuesOf(w, projectName, u) {
604
656
  }
605
657
  ]
606
658
  });
607
- if (s.source === "lib" && w.days && u.uses === 0 && !s.missing)
659
+ if (s.source === "lib" && w.days && u.uses === 0 && !s.missing && (s.added ?? 0) <= Date.now() - 30 * 24 * 60 * 60 * 1e3)
608
660
  issues.push({
609
661
  id: `unused:${s.name}`,
610
662
  severity: "hint",
@@ -615,6 +667,53 @@ function issuesOf(w, projectName, u) {
615
667
  });
616
668
  return issues;
617
669
  }
670
+ function agentSkillFix(w) {
671
+ return {
672
+ label: w.agentSkill === "outdated" ? "Update the skilllib skill" : "Install the skilllib skill for your agents",
673
+ 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.",
674
+ run: (w2) => w2.ops.installAgentSkill()
675
+ };
676
+ }
677
+ function removeAgentSkillFix() {
678
+ return {
679
+ label: "Remove the skilllib skill",
680
+ 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.",
681
+ run: (w) => w.ops.removeAgentSkill()
682
+ };
683
+ }
684
+ function agentSkillIssue(w) {
685
+ if (w.agentSkill === "installed" || !w.agents.length) return null;
686
+ const missing = w.agentSkill === "missing";
687
+ return {
688
+ id: "agent-skill",
689
+ severity: missing ? "hint" : "warning",
690
+ title: missing ? "Your agents don't know about skilllib" : "The skilllib skill for your agents is out of date",
691
+ short: missing ? "Your agents don't know about skilllib" : "Out of date",
692
+ decision: missing,
693
+ fixes: [agentSkillFix(w)]
694
+ };
695
+ }
696
+ function runAll(w, fixes) {
697
+ const held = [];
698
+ const bad = [];
699
+ for (const fix of fixes) {
700
+ let r;
701
+ try {
702
+ r = fix.run(w);
703
+ } catch (e) {
704
+ r = failed(e.message);
705
+ }
706
+ if (isFailure(r)) bad.push(said(r));
707
+ else if (followUp(r)) held.push(said(r));
708
+ }
709
+ const ok = fixes.length - held.length - bad.length;
710
+ const message = [
711
+ `Applied ${ok}${ok === fixes.length ? "" : ` of ${fixes.length}`} fix${fixes.length === 1 ? "" : "es"}${ok ? " (anything removed is in Settings \u203A Backups)" : ""}`,
712
+ held.length ? `${held.join("; ")} (fix ${held.length === 1 ? "it" : "them"} on ${held.length === 1 ? "its" : "their"} own to go ahead)` : "",
713
+ bad.length ? `couldn't: ${bad.join("; ")}` : ""
714
+ ].filter(Boolean).join(" \xB7 ");
715
+ return bad.length ? failed(message) : message;
716
+ }
618
717
  function worst(issues) {
619
718
  return [...issues].sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity])[0];
620
719
  }
@@ -643,7 +742,7 @@ function deleteLibraryFix(w, names) {
643
742
  "Every copy goes to Settings \u203A Backups.",
644
743
  others.length ? `Untracked or the team's own copies in ${others.join(", ")} stay.` : ""
645
744
  ].filter(Boolean).join(" "),
646
- run: (w2) => names.map((n) => w2.ops.deleteLibrary(n)).map((r) => typeof r === "string" ? r : r.message).join(" \xB7 ")
745
+ run: (w2) => joined(names.map((n) => w2.ops.deleteLibrary(n)))
647
746
  };
648
747
  }
649
748
  function replacePluginFix(w, id) {
@@ -690,6 +789,7 @@ function groupCandidates(w, u) {
690
789
  if (m?.source === "claude.ai") return [{ key: "claude.ai", label: "claude.ai skills" }];
691
790
  if (m?.source === "cursor") return [{ key: "cursor", label: "Cursor built-ins" }];
692
791
  if (m?.source === "system") return [{ key: `system:${m.where}`, label: m.where }];
792
+ if (m?.source === "skilllib") return [];
693
793
  if (m) {
694
794
  const origin = originGroup(m.origin);
695
795
  return [...origin ? [origin] : [], ...byTime("global", m.installed), ...byWord("global", m.name)];
@@ -733,10 +833,8 @@ function addRows(w, target, q) {
733
833
  name: q ? `+ Create "${q}"${where}` : "+ Create a new skill (type its name)",
734
834
  note: "",
735
835
  description: "",
736
- run: (w2) => {
737
- const made = w2.ops.createSkill(q);
738
- return target ? w2.ops.add(target, q) : made;
739
- }
836
+ run: (w2) => w2.ops.createSkill(q),
837
+ ...target ? { afterEdit: (w2) => w2.ops.add(target, q) } : {}
740
838
  },
741
839
  { key: "agent", agent: true, name: q ? `\u2726 Write "${q}" with an agent` : "\u2726 Write a new skill with an agent", note: "", description: "" }
742
840
  ];
@@ -745,9 +843,32 @@ function addRows(w, target, q) {
745
843
  }
746
844
 
747
845
  // src/tui/prompts.ts
748
- function reviewPrompt(w, names, scope) {
846
+ var agentNames2 = (agents) => agents.map((a) => harness(a).name).join(", ") || "none of my agents";
847
+ 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)";
848
+ 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)");
849
+ function reviewPrompt(w, names, scope, repo) {
749
850
  const skills = names.map((name) => {
750
851
  const copies = w.machine.filter((m2) => m2.name === name);
852
+ const here = repo ? w.projects.find((p) => p.name === repo)?.skills.find((s) => s.name === name) : void 0;
853
+ if (here) {
854
+ const lib3 = w.library.find((l) => l.name === name);
855
+ return {
856
+ name,
857
+ description: w.descriptions[name] ?? "",
858
+ source: localSource(here),
859
+ installedHow: localInstalled(here),
860
+ path: here.path,
861
+ links: [],
862
+ loadedBy: `${agentNames2(here.agents)} \u2014 in ${repo}`,
863
+ vendor: null,
864
+ usesTotal: w.days ? totalUses(w, name) : null,
865
+ usesByProject: Object.entries(w.usage).flatMap(([p, u]) => u[name] ? [[p, u[name]]] : []),
866
+ installedIn: w.projects.filter((p) => p.skills.some((s) => s.name === name)).map((p) => p.name),
867
+ otherCopies: copies.map((c) => `${c.source} ${c.where}`),
868
+ inYourSkills: lib3 ? `yes, v${lib3.latest}` : null,
869
+ keptGlobal: void 0
870
+ };
871
+ }
751
872
  const m = copies.find((c) => c.source === "global") ?? copies[0];
752
873
  const lib2 = w.library.find((l) => l.name === name);
753
874
  const repos = w.projects.filter((p) => p.skills.some((s) => s.name === name));
@@ -755,11 +876,11 @@ function reviewPrompt(w, names, scope) {
755
876
  return {
756
877
  name,
757
878
  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"),
879
+ 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",
880
+ installedHow: m?.origin ?? (m ? m.where : lib2 ? "created or imported into skilllib" : local ? localInstalled(local) : "committed in the repo"),
760
881
  path: m?.path ?? (lib2 ? w.ops.libraryFile(name).replace(/\/SKILL\.md$/, "") : local?.path ?? ""),
761
882
  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"}`,
883
+ 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
884
  vendor: m && m.source !== "global" ? "turn it off at its source (/plugin in Claude Code, claude.ai settings, or Cursor)" : null,
764
885
  usesTotal: w.days ? totalUses(w, name) : null,
765
886
  usesByProject: Object.entries(w.usage).flatMap(([p, u]) => u[name] ? [[p, u[name]]] : []),
@@ -792,9 +913,7 @@ Keep it under 200 lines. ${p ? "skilllib will offer to import it into my library
792
913
 
793
914
  // src/tui/App.tsx
794
915
  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
- }
916
+ var tildify3 = (p) => tildify(p);
798
917
  var PLACES = [
799
918
  ["projects", "Projects"],
800
919
  ["library", "Your skills"],
@@ -826,6 +945,7 @@ function tag(u, w) {
826
945
  if (s) return [s.source, color.blue];
827
946
  if (u.source === "global") return [u.machine?.kept ? "global \u2713" : "global", color.yellow];
828
947
  if (u.source === "plugin") return [`\u29C9 ${u.where.split("@")[0]}`, color.magenta];
948
+ if (u.source === "skilllib") return ["\u25C6 skilllib", color.accent];
829
949
  return [u.source, color.muted];
830
950
  }
831
951
  function isNameChar(input, key) {
@@ -876,7 +996,7 @@ function TitleRow({ cells, width }) {
876
996
  }
877
997
  var fixOption = (fix) => ({ label: fix.label, fix });
878
998
  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 };
999
+ var COPY_RANK = { lib: 0, repo: 0, untracked: 0, global: 1, skilllib: 1, plugin: 2, "claude.ai": 3, cursor: 3, system: 3 };
880
1000
  function byName(copies) {
881
1001
  const groups = /* @__PURE__ */ new Map();
882
1002
  for (const c of copies) groups.set(c.name, [...groups.get(c.name) ?? [], c]);
@@ -944,7 +1064,7 @@ function skillItem(w, copies, issues, nameW, actions, repo) {
944
1064
  const b = builder(u.name);
945
1065
  for (const l of wrap(w.descriptions[u.name] || "(no description)", width - 4, 3)) b.line([l, color.muted]);
946
1066
  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]);
1067
+ 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
1068
  else b.issues(issues);
949
1069
  b.actions(actions, width);
950
1070
  b.line();
@@ -953,7 +1073,7 @@ function skillItem(w, copies, issues, nameW, actions, repo) {
953
1073
  b.line(["Loaded from", color.accent]);
954
1074
  for (const c of copies) {
955
1075
  const s = c.local;
956
- const path = s ? `${s.dir}/${c.name}` : c.source === "global" ? `${c.where}/${c.name}` : c.where;
1076
+ const path = s ? `${s.dir}/${c.name}` : c.source === "global" || c.source === "skilllib" ? `${c.where}/${c.name}` : c.where;
957
1077
  const extra = s ? s.missing ? "missing" : s.git ? GIT_LABEL[s.git] : "" : "";
958
1078
  const [ct, cc] = c === u ? ["", void 0] : tag(c, w);
959
1079
  b.line([" ", void 0], [path + " ", color.text], [ct ? ct + " " : "", cc], [extra, color.faint]);
@@ -967,7 +1087,10 @@ function projectSkillItems(w, p, nameW, ui) {
967
1087
  (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
1088
  );
969
1089
  }
970
- var reviewOption = (w, name, ui) => ({ label: "Review prompt", action: () => ui.copy(reviewPrompt(w, [name], `Review the skill ${name}.`), `a review prompt for ${name}`) });
1090
+ var reviewOption = (w, name, ui, repo) => ({
1091
+ label: "Review prompt",
1092
+ action: () => ui.copy(reviewPrompt(w, [name], `Review the skill ${name}${repo ? ` in ${repo}` : ""}.`, repo), `a review prompt for ${name}`)
1093
+ });
971
1094
  function versionsOption(w, repo, name, installed, ui) {
972
1095
  return {
973
1096
  label: "Other versions\u2026",
@@ -980,27 +1103,35 @@ function versionsOption(w, repo, name, installed, ui) {
980
1103
  function skillActions(w, p, u, ui) {
981
1104
  const s = u.local;
982
1105
  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) };
1106
+ const remove = {
1107
+ // Its folder gone: all that's left to remove is the skilllib.json entry (the label Health offers too).
1108
+ label: s?.missing ? "Remove it from skilllib.json" : "Remove from repo",
1109
+ 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.`,
1110
+ run: (w2) => w2.ops.remove(p, u.name)
1111
+ };
984
1112
  if (s?.source === "lib")
985
1113
  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),
1114
+ // A tracked skill your library doesn't have: nothing to edit there, no versions to pick.
1115
+ ...inLibrary ? [
1116
+ ...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) })] : [],
1117
+ { label: "Edit in library", action: () => ui.edit(w.ops.libraryFile(u.name)) },
1118
+ versionsOption(w, p, u.name, s.version, ui)
1119
+ ] : [],
989
1120
  fixOption(remove),
990
- reviewOption(w, u.name, ui)
1121
+ reviewOption(w, u.name, ui, p)
991
1122
  ];
992
1123
  if (s?.source === "repo")
993
1124
  return [
994
1125
  ...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
1126
  { label: "Edit SKILL.md", action: () => ui.edit(`${s.path}/SKILL.md`) },
996
- reviewOption(w, u.name, ui)
1127
+ reviewOption(w, u.name, ui, p)
997
1128
  ];
998
1129
  if (s)
999
1130
  return [
1000
1131
  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
1132
  { label: "Edit SKILL.md", action: () => ui.edit(`${s.path}/SKILL.md`) },
1002
1133
  fixOption(remove),
1003
- reviewOption(w, u.name, ui)
1134
+ reviewOption(w, u.name, ui, p)
1004
1135
  ];
1005
1136
  return [{ label: "Open in Global", action: () => ui.openInGlobal(u.name) }, reviewOption(w, u.name, ui)];
1006
1137
  }
@@ -1009,7 +1140,7 @@ function overview(w, p) {
1009
1140
  const info = w.ops.repoInfo(p);
1010
1141
  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];
1011
1142
  return [
1012
- ["Folder", [[tildify2(project2.path), color.text]]],
1143
+ ["Folder", [[tildify3(project2.path), color.text]]],
1013
1144
  ["Remote", [[info.remote ?? "none", info.remote ? color.blue : color.faint]]],
1014
1145
  ["Branch", [[info.branch ?? "\u2013", color.text], [info.dirty ? ` ${info.dirty} uncommitted` : "", color.yellow]]],
1015
1146
  ["Manifest", [manifest]]
@@ -1060,7 +1191,7 @@ function globalActions(w, u, ui) {
1060
1191
  return [
1061
1192
  ...machineActions(m).map(fixOption),
1062
1193
  ...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`) }],
1194
+ ...m.source === "global" || m.source === "skilllib" ? [] : [{ label: atSource, action: () => ui.notify(`${atSource}; skilllib picks up the change next time`) }],
1064
1195
  ...m.source === "global" && !m.broken ? [{ label: "Edit SKILL.md", action: () => ui.edit(`${m.path}/SKILL.md`) }] : [],
1065
1196
  reviewOption(w, m.name, ui)
1066
1197
  ];
@@ -1068,8 +1199,9 @@ function globalActions(w, u, ui) {
1068
1199
  function libraryItems(w, nameW, ui) {
1069
1200
  return w.library.map((l) => {
1070
1201
  const repos = w.projects.filter((p) => p.skills.some((s) => s.name === l.name));
1202
+ const removable = repos.filter((p) => p.skills.some((s) => s.name === l.name && s.source !== "repo"));
1071
1203
  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 ") };
1204
+ 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
1205
  const delFix = deleteLibraryFix(w, [l.name]);
1074
1206
  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
1207
  const behind = repos.filter((p) => p.skills.some((s) => s.name === l.name && s.source === "lib" && s.version < l.latest)).length;
@@ -1101,13 +1233,13 @@ function libraryItems(w, nameW, ui) {
1101
1233
  b.issues(issues);
1102
1234
  b.actions([
1103
1235
  fixOption(addFix),
1104
- ...repos.length ? [
1236
+ ...removable.length ? [
1105
1237
  fixOption({
1106
1238
  label: "Remove from repos\u2026",
1107
1239
  preview: `Pick repos to remove ${l.name} from. Your library keeps it.`,
1108
1240
  run: () => "",
1109
- candidates: () => repos.map((p) => p.name),
1110
- pickRepos: (w2, rs) => rs.map((r) => said(w2.ops.remove(r, l.name))).join(" \xB7 ")
1241
+ candidates: () => removable.map((p) => p.name),
1242
+ pickRepos: (w2, rs) => joined(rs.map((r) => w2.ops.remove(r, l.name)))
1111
1243
  })
1112
1244
  ] : [],
1113
1245
  fixOption(delFix),
@@ -1123,15 +1255,39 @@ function libraryItems(w, nameW, ui) {
1123
1255
  }
1124
1256
  function healthItems(w, nameW, ui) {
1125
1257
  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))];
1258
+ return [...w.projects.flatMap((p) => flagged(p.name, projectSkillItems(w, p.name, nameW, ui))), ...flagged("Global", globalItems(w, nameW, ui)), ...agentSkillItems(w, nameW)];
1259
+ }
1260
+ function agentSkillItems(w, nameW) {
1261
+ const issue = agentSkillIssue(w);
1262
+ if (!issue || w.machine.some((m) => m.source === "skilllib")) return [];
1263
+ const sev = SEV[issue.severity];
1264
+ return [
1265
+ {
1266
+ key: "agent-skill",
1267
+ name: "skilllib",
1268
+ issues: [issue],
1269
+ uses: 0,
1270
+ cells: [
1271
+ { text: sev.icon, width: 2, color: sev.color },
1272
+ { text: clip("skilllib", nameW), width: nameW },
1273
+ { text: clip("Your agents", 16), width: 16, color: color.muted },
1274
+ { text: " " + issue.short, grow: true, color: sev.color }
1275
+ ],
1276
+ detail: (width) => {
1277
+ const b = builder("skilllib");
1278
+ 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]);
1279
+ b.line();
1280
+ b.issues([issue]);
1281
+ return b.d;
1282
+ }
1283
+ }
1284
+ ];
1127
1285
  }
1128
1286
  function addCursor(rows, cursor) {
1129
1287
  const i = Math.min(cursor, rows.length - 1);
1130
1288
  return rows[i] && !rows[i].header ? i : Math.max(0, rows.findIndex((r) => !r.header));
1131
1289
  }
1132
- function said(r) {
1133
- return typeof r === "string" ? r : r.message;
1134
- }
1290
+ var said2 = said;
1135
1291
  function App({ initial, reload, loadUsage: loadUsage2 }) {
1136
1292
  const { exit, suspendTerminal } = useApp();
1137
1293
  const { columns: termCols, rows: termRows } = useWindowSize();
@@ -1169,12 +1325,14 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1169
1325
  notify: setToast,
1170
1326
  openInGlobal,
1171
1327
  edit,
1172
- copy: (text, what) => setToast(said(world.ops.copy(text, what))),
1328
+ copy: (text, what) => setToast(said2(world.ops.copy(text, what))),
1173
1329
  menu: (title, options) => setModal({ kind: "menu", title, options, cursor: 0 })
1174
1330
  };
1175
1331
  const flagged = [
1176
1332
  ...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 })))
1333
+ ...world.machine.flatMap((m) => machineIssues(world, m).map((issue) => ({ key: `Global:${m.name}`, issue }))),
1334
+ // The skilllib skill's issue is on its Global row when it has one (see agentSkillItems).
1335
+ ...[agentSkillIssue(world)].flatMap((issue) => issue ? [{ key: "Global:skilllib", issue }] : [])
1178
1336
  ];
1179
1337
  const healthCount = new Set(flagged.map((f) => f.key)).size;
1180
1338
  const healthWorst = worst(flagged.map((f) => f.issue));
@@ -1203,16 +1361,18 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1203
1361
  if (items[cursor]?.header) cursor = firstSelectable;
1204
1362
  const current = items[cursor];
1205
1363
  const detail = current?.detail?.(columns);
1206
- function apply(run) {
1364
+ function apply(run, w = world) {
1207
1365
  let result;
1208
1366
  try {
1209
- result = run(world);
1367
+ result = run(w);
1210
1368
  } catch (e) {
1211
- result = `Couldn't do it: ${e.message}`;
1369
+ result = failed(`Couldn't do it: ${e.message}`);
1212
1370
  }
1213
1371
  setBase(reload());
1214
- setToast(`\u2713 ${said(result)}`);
1215
- if (typeof result !== "string") setModal({ kind: "confirm", fix: result.then });
1372
+ const then = followUp(result);
1373
+ const toast2 = `${isFailure(result) ? "\u2717" : then ? "!" : "\u2713"} ${said2(result)}`;
1374
+ setToast(toast2);
1375
+ if (then) setModal({ kind: "confirm", fix: then, toast: toast2 });
1216
1376
  }
1217
1377
  function choose(o) {
1218
1378
  if (o.action) return o.action();
@@ -1232,6 +1392,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1232
1392
  function fixSelected() {
1233
1393
  const issue = current && worst(current.issues);
1234
1394
  if (!issue) return setToast("Nothing to fix \xB7 enter shows what you can do");
1395
+ if (!issue.fixes.length) return setToast(`Nothing skilllib can do here: ${issue.short}`);
1235
1396
  if (issue.decision) {
1236
1397
  setFocus("detail");
1237
1398
  setDetailCursor(Math.max(0, detail?.options.findIndex((o) => o.issue === issue) ?? 0));
@@ -1308,7 +1469,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1308
1469
  preview: `Pick repos. These ${n(mine.length)} go into your library and those repos, and stop loading globally (originals backed up).`,
1309
1470
  run: () => "",
1310
1471
  preticked: usedIn,
1311
- pickRepos: (w, repos) => (mine.forEach((m) => w.ops.moveGlobal(m, repos, label)), `${n(mine.length)} now load only in ${repos.join(", ")}`)
1472
+ pickRepos: (w, repos) => w.ops.moveGlobal(mine, repos, label)
1312
1473
  }),
1313
1474
  ...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
1475
  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,7 +1480,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1319
1480
  preview: `Install these ${n(libs.length)} into the repos you pick.`,
1320
1481
  run: () => "",
1321
1482
  preticked: () => [],
1322
- pickRepos: (w, repos) => (repos.forEach((r) => libs.forEach((l) => w.ops.add(r, l.name))), `Added ${n(libs.length)} to ${repos.join(", ")}`)
1483
+ pickRepos: (w, repos) => w.ops.addTo(repos, libs.map((l) => l.name))
1323
1484
  })
1324
1485
  ] : [],
1325
1486
  ...libs.length ? [fixOption(deleteLibraryFix(world, libs.map((l) => l.name)))] : [],
@@ -1359,9 +1520,9 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1359
1520
  `\u22EF ${repo}\u2026`,
1360
1521
  ["Open its folder, hide it from the list, or copy a review prompt for its skills."],
1361
1522
  () => ui.menu(repo, [
1362
- { label: "Open its folder", action: () => setToast(said(world.ops.openFolder(project(world, repo).path))) },
1523
+ { label: "Open its folder", action: () => setToast(said2(world.ops.openFolder(project(world, repo).path))) },
1363
1524
  { 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}`) }] : []
1525
+ ...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
1526
  ])
1366
1527
  )
1367
1528
  ];
@@ -1393,19 +1554,21 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1393
1554
  setQuery("");
1394
1555
  setChip("all");
1395
1556
  }
1396
- function edit(file) {
1397
- if (!existsSync2(file)) return setToast(`${tildify2(file)} doesn't exist`);
1557
+ function edit(file, then) {
1558
+ if (!existsSync2(file)) return then ? apply(then) : (setBase(reload()), setToast(`${tildify3(file)} doesn't exist`));
1398
1559
  const editor = process.env.VISUAL || process.env.EDITOR || (process.platform === "win32" ? "notepad" : "vi");
1399
1560
  void suspendTerminal(() => {
1400
1561
  if (process.platform === "win32") spawnSync2(`${editor} "${file}"`, { shell: true, stdio: "inherit" });
1401
1562
  else spawnSync2("/bin/sh", ["-c", `${editor} "$1"`, "sh", file], { stdio: "inherit" });
1402
1563
  }).then(() => {
1403
- setBase(reload());
1404
- setToast(`Saved ${tildify2(file)}`);
1564
+ const fresh = reload();
1565
+ if (then) return apply(then, fresh);
1566
+ setBase(fresh);
1567
+ setToast(`Saved ${tildify3(file)}`);
1405
1568
  });
1406
1569
  }
1407
1570
  function askForFolder(title) {
1408
- setModal({ kind: "input", title, value: "~/Projects", submit: (v) => apply((w) => w.ops.addRoot(v)) });
1571
+ setModal({ kind: "input", title, value: "", placeholder: "~/Projects", submit: (v) => apply((w) => w.ops.addRoot(v)) });
1409
1572
  }
1410
1573
  useEffect(() => {
1411
1574
  const folder = () => !initial.roots.length && askForFolder("Where are your projects? skilllib finds the git repos in there");
@@ -1434,20 +1597,27 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1434
1597
  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
1598
  })
1436
1599
  ),
1600
+ // The skill that lets your agents use skilllib: installing adds a global skill, so it asks first.
1601
+ row(
1602
+ "agent-skill",
1603
+ `${world.agentSkill === "installed" ? "[x]" : "[ ]"} \u25C6 skilllib skill`,
1604
+ 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",
1605
+ () => world.agentSkill === "installed" ? setToast("Installed \xB7 `skilllib agent-skill remove` takes it out") : choose(fixOption(agentSkillFix(world)))
1606
+ ),
1437
1607
  header("#roots", "Project folders"),
1438
1608
  ...world.roots.map(
1439
1609
  (r) => row(
1440
1610
  `root:${r}`,
1441
- tildify2(r),
1611
+ tildify3(r),
1442
1612
  "scanned for git repos",
1443
- () => ui.menu(tildify2(r), [
1613
+ () => ui.menu(tildify3(r), [
1444
1614
  { 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) })
1615
+ 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
1616
  ])
1447
1617
  )
1448
1618
  ),
1449
1619
  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))))] : [],
1620
+ ...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
1621
  header("#backups", "Backups"),
1452
1622
  ...world.backups.length ? world.backups.map(
1453
1623
  (b, i) => row(`backup:${i}`, `\u21BA ${b.name}`, `${b.from} \xB7 ${b.at}`, () => apply((w) => w.ops.restoreBackup(i)))
@@ -1551,7 +1721,11 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1551
1721
  }
1552
1722
  if (m.kind === "input") {
1553
1723
  if (key.escape) return setModal(null);
1554
- if (key.return) return setModal(null), m.submit(m.value.trim());
1724
+ if (key.return) {
1725
+ const value = m.value.trim() || m.placeholder;
1726
+ setModal(null);
1727
+ return value ? m.submit(value) : void 0;
1728
+ }
1555
1729
  if (key.backspace || key.delete) return setModal({ ...m, value: m.value.slice(0, -1) });
1556
1730
  if (key.ctrl && input === "u") return setModal({ ...m, value: "" });
1557
1731
  if (input && !key.ctrl && !key.meta) return setModal({ ...m, value: m.value + input });
@@ -1564,7 +1738,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1564
1738
  if (key.return && m.options[m.cursor]) return setModal(null), choose(m.options[m.cursor]);
1565
1739
  return;
1566
1740
  }
1567
- if (key.escape) return setModal(null);
1741
+ if (key.escape) return setModal(null), m.kind === "confirm" && m.toast && setToast(m.toast);
1568
1742
  if (m.kind === "confirm") {
1569
1743
  if (key.return || input === "y") {
1570
1744
  setModal(null);
@@ -1601,17 +1775,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1601
1775
  if (key.return) {
1602
1776
  setModal(null);
1603
1777
  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
- });
1778
+ apply((w) => runAll(w, chosen.map((it) => it.fix)));
1615
1779
  }
1616
1780
  return;
1617
1781
  }
@@ -1634,9 +1798,9 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1634
1798
  const row = rows[cursor2];
1635
1799
  if (row.create && !/^[a-z0-9]/.test(m.query)) return setToast("Type the new skill's name first");
1636
1800
  setModal(null);
1637
- apply(row.run);
1638
- if (row.create) edit(world.ops.libraryFile(m.query));
1639
- return;
1801
+ if (!row.create) return apply(row.run);
1802
+ row.run(world);
1803
+ return edit(world.ops.libraryFile(m.query), row.afterEdit);
1640
1804
  }
1641
1805
  if (isNameChar(input, key)) return setModal({ ...m, query: m.query + input.toLowerCase(), cursor: 0 });
1642
1806
  }
@@ -1749,8 +1913,11 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1749
1913
  );
1750
1914
  else if (modal?.kind === "input")
1751
1915
  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" })
1916
+ modal.value || !modal.placeholder ? /* @__PURE__ */ jsx2(Text2, { color: color.text, children: modal.value + "\u258F" }) : /* @__PURE__ */ jsxs2(Text2, { children: [
1917
+ "\u258F",
1918
+ /* @__PURE__ */ jsx2(Text2, { color: color.faint, children: modal.placeholder })
1919
+ ] }),
1920
+ /* @__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
1921
  ] });
1755
1922
  else if (modal?.kind === "agents")
1756
1923
  body = /* @__PURE__ */ jsx2(
@@ -1877,7 +2044,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1877
2044
  ] })
1878
2045
  ] }),
1879
2046
  /* @__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 }),
2047
+ /* @__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
2048
  toast ? null : /* @__PURE__ */ jsx2(Text2, { color: color.faint, children: hints + " " })
1882
2049
  ] })
1883
2050
  ] });
@@ -1936,9 +2103,8 @@ function Help({ width, height }) {
1936
2103
  import { execFileSync, spawn, spawnSync as spawnSync3 } from "child_process";
1937
2104
  import { existsSync as existsSync3, lstatSync, realpathSync } from "fs";
1938
2105
  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, "/");
2106
+ function tildify4(p) {
2107
+ return tildify(p).replace(/\\/g, "/");
1942
2108
  }
1943
2109
  function uniqueNames(roots) {
1944
2110
  const tail = (r, n) => r.split(/[\\/]/).filter(Boolean).slice(-n).join("/");
@@ -1956,25 +2122,37 @@ function localSkill(root, s, git2) {
1956
2122
  "update available": {},
1957
2123
  "edited locally": { edited: true },
1958
2124
  "edited locally, update available": { edited: true },
1959
- "folder missing": { missing: true },
2125
+ // sync restores it from your library, unless the library doesn't have it (a teammate's skill, another library).
2126
+ "folder missing": { missing: true, ...s.latest === null && { library: "missing" } },
1960
2127
  "not in library": { library: "missing" }
1961
2128
  };
1962
2129
  const other = {
1963
2130
  "untracked copy of library skill": { source: "untracked", library: "same" },
1964
2131
  "untracked, differs from library": { source: "untracked", library: "differs" },
1965
2132
  "local only": { source: "untracked" },
2133
+ // Installed with `npx skills add` (the repo's skills-lock.json): not skilllib's, so not a library skill.
2134
+ "from npx skills": { source: "untracked" },
1966
2135
  "repo skill": { source: "repo" },
1967
2136
  "repo skill, in library": { source: "repo", library: "same" },
1968
2137
  "repo skill, differs from library": { source: "repo", library: "differs" }
1969
2138
  };
2139
+ const state = git2 ? git2.of(relativeTo(root, s.path)) : null;
2140
+ const own = other[s.state]?.source === "untracked" && (state === "committed" || state === "changed") ? { source: "repo" } : {};
2141
+ const blind = s.state === "folder missing" ? [] : s.visibility.filter((v) => v.paths === 0).map((v) => v.id);
2142
+ const uncommitted = blind.length ? linkEverywhere(root, s.name, s.location, { dryRun: true }).uncommitted : [];
2143
+ const unshared = blind.filter((id) => uncommitted.includes(harness(id).projectDirs[0]));
1970
2144
  return {
1971
2145
  name: s.name,
1972
2146
  source: "lib",
1973
2147
  dir: s.location,
1974
2148
  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
2149
+ // Agents that actually load it here: visibility lists every agent you use, with `paths: 0` for the blind ones.
2150
+ agents: s.visibility.filter((v) => v.paths > 0).map((v) => v.id),
2151
+ ...s.managed ? { library: "same", version: s.version ?? void 0, ...managed[s.state] } : { ...other[s.state], ...own },
2152
+ ...unshared.length && { unshared },
2153
+ ...s.source ? { origin: `npx skills: ${s.source}` } : {},
2154
+ added: addedAt(s.path),
2155
+ git: state
1978
2156
  };
1979
2157
  }
1980
2158
  var partsOf = /* @__PURE__ */ new Map();
@@ -2004,8 +2182,8 @@ function machineSkill(s, kept) {
2004
2182
  const global = s.kind === "global" || s.kind === "skills.sh";
2005
2183
  return {
2006
2184
  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,
2185
+ source: global ? "global" : s.kind === "plugin" ? "plugin" : s.kind === "claude.ai" ? "claude.ai" : s.kind === "system" ? "system" : s.kind === "skilllib" ? "skilllib" : "cursor",
2186
+ 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
2187
  ...s.kind === "skills.sh" ? { origin: `npx skills: ${s.origin}` } : {},
2010
2188
  path: s.path,
2011
2189
  links: s.links,
@@ -2020,14 +2198,13 @@ function dupesOf(r, root, git2 = null) {
2020
2198
  const out = /* @__PURE__ */ new Map();
2021
2199
  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
2200
  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 })) });
2201
+ 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
2202
  return out;
2025
2203
  }
2026
2204
  function loadWorld() {
2027
2205
  forgetLatest();
2028
2206
  const agents = enabledHarnesses();
2029
- const cwdRoot = findProjectRoot();
2030
- const here = isProjectCandidate(cwdRoot) && (existsSync3(join2(cwdRoot, ".git")) || existsSync3(join2(cwdRoot, "skilllib.json"))) ? cwdRoot : null;
2207
+ const here = projectHere();
2031
2208
  const real = (p) => {
2032
2209
  try {
2033
2210
  return realpathSync(p);
@@ -2090,10 +2267,11 @@ function loadWorld() {
2090
2267
  library,
2091
2268
  usage: {},
2092
2269
  descriptions,
2093
- backups: backups.map((b) => ({ name: b.name, from: tildify3(b.from), at: b.movedAt })),
2270
+ backups: backups.map((b) => ({ name: b.name, from: tildify4(b.from), at: b.movedAt })),
2094
2271
  roots: readConfig().roots,
2095
2272
  hidden: readConfig().hidden,
2096
2273
  agentsChosen: harnessesChosen(),
2274
+ agentSkill: agentSkillState(agents),
2097
2275
  ops: realOps(new Map(projects.map((p) => [p.name, p.path])), backups)
2098
2276
  };
2099
2277
  }
@@ -2125,44 +2303,99 @@ function remoteLabel(url) {
2125
2303
  if (!url) return void 0;
2126
2304
  return url.replace(/^[a-z+]+:\/\//, "").replace(/^[^@/]+@/, "").replace(/:(?!\d)/, "/").replace(/\.git$/, "");
2127
2305
  }
2128
- function said2(c, repo) {
2306
+ function said3(c, repo) {
2129
2307
  if (c.action === "skipped") return `${c.name}: ${c.reason ?? "skipped"}`;
2130
2308
  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}`;
2309
+ const back = c.action === "updated" && c.from !== void 0 && c.to !== void 0 && c.to < c.from;
2310
+ 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}`;
2311
+ return c.backedUp ? `${done} (your edits are in Settings \u203A Backups)` : done;
2132
2312
  }
2133
2313
  function realOps(roots, backups) {
2134
2314
  const rootOf = (repo) => roots.get(repo);
2135
2315
  const find = (repo, name) => projectStatus(rootOf(repo)).find((s) => s.name === name);
2136
2316
  const info = /* @__PURE__ */ new Map();
2317
+ const result = (c, repo) => c.action === "skipped" ? failed(said3(c, repo)) : said3(c, repo);
2318
+ const copyDir = (repo, name) => readManifest(rootOf(repo)).skills[name]?.dir ?? PROJECT_SKILLS_DIR;
2137
2319
  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) };
2320
+ const it = (dirs) => dirs.length === 1 ? "it" : "them";
2321
+ if (c.uncommitted?.length)
2322
+ 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`;
2323
+ if (!c.blocked?.length) return result(c, repo);
2324
+ return { message: `${said3(c, repo)}; not linked in ${c.blocked.join(", ")}: git tracks ${it(c.blocked)}`, then: allowTracked([{ repo, name, blocked: c.blocked }]) };
2140
2325
  };
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);
2326
+ const addTo = (repos, names) => {
2327
+ if (repos.length === 1 && names.length === 1) return withLinks(addSkill(rootOf(repos[0]), names[0]), repos[0], names[0]);
2328
+ const changes = repos.flatMap((repo) => names.map((name) => ({ repo, name, c: addSkill(rootOf(repo), name) })));
2329
+ const done = changes.filter((x) => x.c.action !== "skipped");
2330
+ const added = [...new Set(done.map((x) => x.name))];
2331
+ 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(", ")}` : "";
2332
+ const notes = [];
2333
+ for (const repo of repos)
2334
+ for (const kind of ["blocked", "uncommitted"]) {
2335
+ const here = done.filter((x) => x.repo === repo && x.c[kind]?.length);
2336
+ if (!here.length) continue;
2337
+ const dirs = [...new Set(here.flatMap((x) => x.c[kind]))];
2338
+ const who = here.length === names.length ? "" : `${here.map((x) => x.name).join(", ")}: `;
2339
+ const copies = [...new Set(here.map((x) => copyDir(repo, x.name)))];
2340
+ 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`;
2341
+ notes.push(`${who}not linked in ${dirs.join(", ")} in ${repo}: ${why}`);
2342
+ }
2343
+ const skipped = changes.filter((x) => x.c.action === "skipped").map((x) => `${x.name} not added to ${x.repo}: ${x.c.reason}`);
2344
+ const text = [summary, ...notes, ...skipped].filter(Boolean).join("; ");
2345
+ const held = done.flatMap((x) => x.c.blocked?.length ? [{ repo: x.repo, name: x.name, blocked: x.c.blocked }] : []);
2346
+ if (!done.length) return failed(text);
2347
+ if (held.length) return { message: text, then: allowTracked(held) };
2348
+ return skipped.length ? failed(text) : text;
2349
+ };
2350
+ const allowTracked = (held) => {
2351
+ const repos = [...new Set(held.map((h) => h.repo))];
2352
+ const dirs = [...new Set(held.flatMap((h) => h.blocked))];
2353
+ return {
2354
+ label: `Also link ${new Set(held.map((h) => h.name)).size === 1 ? "it" : "them"} in ${dirs.join(", ")}`,
2355
+ 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(", ")}.`,
2356
+ run: (w) => {
2357
+ for (const repo of repos) allowTrackedLinks(rootOf(repo));
2358
+ if (held.length === 1) return w.ops.link(held[0].repo, held[0].name, true);
2359
+ const results = held.map((h) => ({ ...h, r: linkIn(h.repo, h.name, true) }));
2360
+ const linked = results.filter((x) => x.r.created.length);
2361
+ const names = [...new Set(linked.map((x) => x.name))];
2362
+ 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(", ")}` : "";
2363
+ const others = results.filter((x) => !x.r.created.length || x.r.uncommitted.length).map((x) => `${x.repo}: ${linkSaid(x.name, x.r)}`);
2364
+ return [summary, ...others].join("; ");
2365
+ }
2366
+ };
2367
+ };
2368
+ const linkIn = (repo, name, allow) => {
2369
+ const s = find(repo, name);
2370
+ return s.managed ? relinkDependency(rootOf(repo), name, { allowTracked: allow }) : linkEverywhere(rootOf(repo), name, s.location, { allowTracked: allow });
2371
+ };
2372
+ const linkSaid = (name, r) => {
2373
+ const linked = r.created.length ? `linked in ${r.created.join(", ")}` : "";
2374
+ 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` : "";
2375
+ return `${name}: ${[linked, notLinked].filter(Boolean).join("; ") || "nothing to link"}`;
2376
+ };
2377
+ const orForce = (c, repo, label, force) => c.action === "skipped" && c.reason?.includes("edits") ? {
2378
+ message: `${c.name} in ${repo} has local edits`,
2379
+ then: { label, preview: `Your edits to ${c.name} in ${repo} go to Settings \u203A Backups.`, run: () => result(force(), repo) }
2380
+ } : result(c, repo);
2150
2381
  const toLibrary = (path) => {
2151
2382
  const r = importSkill(path, { force: true });
2152
2383
  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
2384
  };
2154
2385
  return {
2155
- add: (repo, name) => withLinks(addSkill(rootOf(repo), name), repo, name),
2386
+ add: (repo, name) => addTo([repo], [name]),
2387
+ addTo,
2156
2388
  remove: (repo, name) => {
2157
2389
  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`;
2390
+ if (!s) return failed(`${name} isn't in ${repo}`);
2391
+ const committed = !s.managed && ["committed", "changed"].includes(gitInfo(rootOf(repo))?.of(relativeTo(rootOf(repo), s.path)) ?? "");
2392
+ 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
2393
  if (!s.managed) return removeUntracked(rootOf(repo), name, s.path), `Removed ${name} from ${repo} (in Settings \u203A Backups)`;
2161
2394
  return orForce(removeSkill(rootOf(repo), name), repo, "Remove it anyway", () => removeSkill(rootOf(repo), name, { force: true }));
2162
2395
  },
2163
2396
  restore: (repo, name) => {
2164
2397
  const c = syncProject(rootOf(repo)).find((x) => x.name === name);
2165
- return c ? said2(c, repo) : `${name} is already in place`;
2398
+ return c ? result(c, repo) : `${name} is already in place`;
2166
2399
  },
2167
2400
  update: (repo, name) => {
2168
2401
  const c = updateProject(rootOf(repo), [name])[0];
@@ -2180,10 +2413,10 @@ function realOps(roots, backups) {
2180
2413
  return withLinks(addSkill(rootOf(repo), name, s.managed && s.version ? { version: s.version } : {}), repo, name);
2181
2414
  },
2182
2415
  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;
2416
+ const r = linkIn(repo, name, allow);
2417
+ const message = linkSaid(name, r);
2418
+ if (!r.blocked.length || allow) return r.created.length ? message : failed(message);
2419
+ 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
2420
  },
2188
2421
  track: (repo, name) => withLinks(addSkill(rootOf(repo), name), repo, name),
2189
2422
  importLocal: (repo, name) => {
@@ -2199,10 +2432,9 @@ function realOps(roots, backups) {
2199
2432
  if (!plan) return `${name}: nothing to tidy`;
2200
2433
  const r = applyTidy(plan, { git: allowGit ? "go" : "keep" });
2201
2434
  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;
2435
+ if (!r.held.length) return r.applied.length ? `${name}: ${steps(r.applied)}` : `${name}: nothing changed`;
2204
2436
  return {
2205
- message,
2437
+ message: `${name}: ${r.applied.length ? `${steps(r.applied)}; ` : ""}held back ${steps(r.held)}, because git tracks it`,
2206
2438
  then: {
2207
2439
  label: "Also change what git tracks",
2208
2440
  preview: `${steps(r.held)}: these changes will show in git status in ${repo}. Replaced copies go to Settings \u203A Backups.`,
@@ -2213,52 +2445,89 @@ function realOps(roots, backups) {
2213
2445
  unloadGlobal: (m) => {
2214
2446
  if (!m.broken && !latestVersion(m.name)) importSkill(m.path);
2215
2447
  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}`;
2448
+ return r.ok ? `${m.name} no longer loads globally (in Settings \u203A Backups)` : failed(`${m.name}: ${r.reason}`);
2217
2449
  },
2218
2450
  deleteGlobal: (m) => {
2219
2451
  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}`;
2452
+ return r.ok ? m.broken ? `Removed the broken link ${m.name}` : `Deleted ${m.name} (in Settings \u203A Backups)` : failed(`${m.name}: ${r.reason}`);
2221
2453
  },
2222
2454
  keepGlobal: (name, keep) => (setKeepGlobal([name], keep), keep ? `${name} marked as global on purpose` : `${name} is no longer marked as global on purpose`),
2223
2455
  linkGlobal: (m, agents) => {
2224
2456
  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}`;
2457
+ const linked = r.linked.map(tildify4).join(", ");
2458
+ const skipped = r.skipped.length ? `; ${r.skipped.map(tildify4).join(", ")} already holds a different skill` : "";
2459
+ const message = `${m.name}: ${linked ? `linked ${linked}` : "nothing linked"}${skipped}`;
2460
+ return !linked && skipped ? failed(message) : message;
2228
2461
  },
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)` : ""}`;
2462
+ moveGlobal: (skills, repos, group) => {
2463
+ const moved = [];
2464
+ const notes = [];
2465
+ const held = [];
2466
+ for (const m of skills) {
2467
+ if (importSkill(m.path).status === "exists") {
2468
+ notes.push(`${m.name}: your library has a different ${m.name}; nothing moved (update your library from this copy first)`);
2469
+ continue;
2470
+ }
2471
+ if (group) recordOrigin(m.name, `group: ${group}`);
2472
+ const results = repos.map((r2) => [r2, addSkill(rootOf(r2), m.name)]);
2473
+ const skipped = results.filter(([, c]) => c.action === "skipped");
2474
+ if (skipped.length) {
2475
+ notes.push(`${m.name}: not added to ${skipped.map(([r2, c]) => `${r2} (${c.reason})`).join(", ")}, so it still loads globally`);
2476
+ continue;
2477
+ }
2478
+ const r = unloadGlobal(m.path, m.links);
2479
+ if (!r.ok) {
2480
+ notes.push(`${m.name}: ${r.reason}`);
2481
+ continue;
2482
+ }
2483
+ moved.push(m.name);
2484
+ for (const [repo, c] of results) {
2485
+ const blind = visibilityOf(rootOf(repo), m.name).flatMap((v) => v.paths === 0 ? [v.id] : []);
2486
+ const blocked = [...c.blocked ?? [], ...c.uncommitted ?? []];
2487
+ if (!blocked.length || !blind.length) continue;
2488
+ held.push({ repo, name: m.name, blocked, blind, copy: copyDir(repo, m.name), shared: !c.uncommitted?.length });
2489
+ }
2490
+ }
2491
+ const gaps = /* @__PURE__ */ new Map();
2492
+ for (const repo of new Set(held.map((h) => h.repo)))
2493
+ for (const shared of [true, false]) {
2494
+ const here = held.filter((h) => h.repo === repo && h.shared === shared);
2495
+ if (!here.length) continue;
2496
+ const agents = [...new Set(here.flatMap((h) => h.blind))].map((id) => harness(id).name).join(", ");
2497
+ const what = skills.length === 1 ? "it" : here.length === moved.length ? "them" : here.map((h) => h.name).join(", ");
2498
+ const dirs = [...new Set(here.flatMap((h) => h.blocked))].join(", ");
2499
+ 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`;
2500
+ const key = `${agents}|${what}|${why}`;
2501
+ if (!gaps.has(key)) gaps.set(key, { who: `${agents} can't see ${what}`, why, repos: [] });
2502
+ gaps.get(key).repos.push(repo);
2503
+ }
2504
+ const blindNotes = [...gaps.values()].map((g) => `${g.who} in ${g.repos.join(", ")}: ${g.why}`);
2505
+ const summary = moved.length ? `${moved.length === 1 ? moved[0] : `${moved.length} skills`} now load${moved.length === 1 ? "s" : ""} only in ${repos.join(", ")}` : "";
2506
+ const text = [summary, ...blindNotes, ...notes].filter(Boolean).join("; ") || "Nothing moved";
2507
+ const askable = held.filter((h) => h.shared);
2508
+ if (!moved.length && notes.length) return failed(text);
2509
+ return askable.length ? { message: text, then: allowTracked(askable) } : text;
2240
2510
  },
2241
2511
  createSkill: (name) => {
2242
2512
  const r = createSkill(name, "");
2243
- return r.ok ? `Created ${name} in your library: edit its SKILL.md from Your skills` : `${name}: ${r.reason}`;
2513
+ return r.ok ? `Created ${name} in your library: edit its SKILL.md from Your skills` : failed(`${name}: ${r.reason}`);
2244
2514
  },
2245
2515
  pluginOffHere: (repo, id) => {
2246
2516
  const root = rootOf(repo);
2247
2517
  const plugin = claudePlugins(skillsLoadedIn(root, machineSkills()), root).find((p) => p.id === id);
2248
- if (!plugin) return `${id} isn't on in ${repo}`;
2518
+ if (!plugin) return failed(`${id} isn't on in ${repo}`);
2249
2519
  const r = turnOffIn(plugin, root);
2250
- if (!r.ok) throw new Error(r.message);
2251
- return r.message;
2520
+ return r.ok ? r.message : failed(r.message);
2252
2521
  },
2253
2522
  replacePlugin: (id, repos) => {
2254
2523
  const skills = machineSkills().filter((s) => s.kind === "plugin" && s.origin === id && !s.broken);
2255
2524
  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`;
2525
+ if (clashes.length) return failed(`${id} kept: your library has different ${clashes.join(", ")}; update or delete ${clashes.length === 1 ? "it" : "them"} first`);
2257
2526
  const skipped = repos.flatMap((r) => skills.flatMap((s) => {
2258
2527
  const c = addSkill(rootOf(r), s.name);
2259
2528
  return c.action === "skipped" ? [`${s.name} in ${r} (${c.reason})`] : [];
2260
2529
  }));
2261
- if (skipped.length) return `${id} kept: not added ${skipped.join(", ")}`;
2530
+ if (skipped.length) return failed(`${id} kept: not added ${skipped.join(", ")}`);
2262
2531
  const copied = `${skills.length} skill${skills.length === 1 ? " from " + id + " is" : "s from " + id + " are"} in your library${repos.length ? ` and in ${repos.join(", ")}` : ""}`;
2263
2532
  const bin = claudeBinary();
2264
2533
  const claude = (...args) => spawnSync3(bin, ["plugin", ...args], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: 2e4 });
@@ -2274,7 +2543,7 @@ function realOps(roots, backups) {
2274
2543
  try {
2275
2544
  setPluginEnabled(id, false);
2276
2545
  } catch (e) {
2277
- return `${copied}; couldn't turn ${id} off (${why}; ${e.message}): turn it off with /plugin`;
2546
+ return failed(`${copied}; couldn't turn ${id} off (${why}; ${e.message}): turn it off with /plugin`);
2278
2547
  }
2279
2548
  return `${copied}; turned ${id} off in ~/.claude/settings.json (${why})${synced ? "" : `: finish with /plugin uninstall ${id}`}`;
2280
2549
  },
@@ -2287,13 +2556,13 @@ function realOps(roots, backups) {
2287
2556
  from.push([...roots].find(([, r2]) => r2 === root)?.[0] ?? basename(root));
2288
2557
  }
2289
2558
  const r = deleteLibrarySkill(name);
2290
- if (!r.ok) return `${name}: ${r.reason}`;
2559
+ if (!r.ok) return failed(`${name}: ${r.reason}`);
2291
2560
  return `Deleted ${name} from your library${from.length ? ` and from ${from.join(", ")}` : ""} (in Settings \u203A Backups)`;
2292
2561
  },
2293
2562
  restoreBackup: (i) => {
2294
2563
  const b = backups[i];
2295
2564
  const r = b.kind === "plugin" ? restorePlugin(b) : restoreBackup(b);
2296
- return r.ok ? `Restored ${b.name} to ${tildify3(r.to)}` : `${b.name}: ${r.reason}`;
2565
+ return r.ok ? `Restored ${b.name} to ${tildify4(r.to)}` : failed(`${b.name}: ${r.reason}`);
2297
2566
  },
2298
2567
  setAgents: (ids) => (setHarnesses(ids), "Saved your agents"),
2299
2568
  installVersion: (repo, name, version) => withLinks(addSkill(rootOf(repo), name, { version, force: true }), repo, name),
@@ -2302,20 +2571,28 @@ function realOps(roots, backups) {
2302
2571
  addRoot: (path) => {
2303
2572
  const root = addRoot(path);
2304
2573
  discoverProjects();
2305
- return `Scanning ${tildify3(root)} for repos`;
2574
+ return `Scanning ${tildify4(root)} for repos`;
2306
2575
  },
2307
- removeRoot: (path) => (removeRoot(path), `Stopped scanning ${tildify3(path)}; repos already found stay until you hide them`),
2576
+ removeRoot: (path) => (removeRoot(path), `Stopped scanning ${tildify4(path)}; repos already found stay until you hide them`),
2308
2577
  rescan: () => (discoverProjects(), "Looked for repos in your project folders again"),
2309
2578
  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`),
2579
+ unhide: (path) => (setHidden(path, false), `${tildify4(path)} is back in the list`),
2311
2580
  openFolder: (path) => {
2312
2581
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
2313
2582
  spawn(cmd, [path], { detached: true, stdio: "ignore" }).unref();
2314
- return `Opened ${tildify3(path)}`;
2583
+ return `Opened ${tildify4(path)}`;
2315
2584
  },
2316
2585
  copy: (text, what) => {
2317
2586
  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)}`;
2587
+ return r.copied ? `Copied ${what}: paste it into Claude Code, Cursor or Codex` : `Couldn't reach the clipboard; ${what} is in ${tildify4(r.savedTo)}`;
2588
+ },
2589
+ installAgentSkill: () => {
2590
+ const r = installAgentSkill();
2591
+ return r.ok ? `Your agents can now use skilllib (${r.dirs.map(tildify4).join(", ")})` : failed(`skilllib skill: ${r.reason}`);
2592
+ },
2593
+ removeAgentSkill: () => {
2594
+ const removed = removeAgentSkill();
2595
+ return removed.length ? `Removed the skilllib skill (${removed.map(tildify4).join(", ")})` : failed("The skilllib skill isn't installed");
2319
2596
  },
2320
2597
  repoInfo: (repo) => {
2321
2598
  if (!info.has(repo)) {