skilllib 1.3.1 → 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,39 +1,53 @@
1
1
  import {
2
2
  HARNESSES,
3
+ PROJECT_SKILLS_DIR,
3
4
  addRoot,
4
5
  addSkill,
6
+ addedAt,
7
+ agentSkillState,
5
8
  allowTrackedLinks,
9
+ applyTidy,
6
10
  claudeBinary,
11
+ claudePlugins,
12
+ copyLabel,
7
13
  createSkill,
14
+ cursorPluginSkills,
8
15
  deleteGlobal,
9
16
  deleteLibrarySkill,
17
+ describeStep,
10
18
  discoverProjects,
11
19
  enabledHarnesses,
12
- findProjectRoot,
13
20
  forgetLatest,
14
21
  gitInfo,
22
+ gitVisibleSteps,
15
23
  harness,
16
24
  harnessesChosen,
17
25
  importSkill,
18
- isProjectCandidate,
26
+ installAgentSkill,
19
27
  keptGlobal,
20
28
  latestVersion,
21
29
  libraryOrigins,
22
30
  librarySkillDir,
23
31
  librarySkills,
24
32
  linkEverywhere,
33
+ linkGlobal,
25
34
  listBackups,
26
35
  machineSkills,
36
+ planGlobalTidy,
37
+ planProjectTidy,
27
38
  pluginBackups,
39
+ projectHere,
28
40
  projectOfFactory,
29
41
  projectStatus,
30
42
  projectsUsing,
31
43
  readConfig,
44
+ readManifest,
32
45
  readSkillInfo,
33
46
  recordOrigin,
34
47
  recordRemovedPlugin,
35
48
  relativeTo,
36
49
  relinkDependency,
50
+ removeAgentSkill,
37
51
  removeRoot,
38
52
  removeSkill,
39
53
  removeUntracked,
@@ -45,13 +59,16 @@ import {
45
59
  setKeepGlobal,
46
60
  setPluginEnabled,
47
61
  skilllibHome,
62
+ skillsLoadedIn,
48
63
  syncProject,
64
+ tildify,
65
+ turnOffIn,
49
66
  unloadGlobal,
50
67
  updateProject,
51
- userHome,
52
68
  versionHistory,
69
+ visibilityOf,
53
70
  visibleProjects
54
- } from "./chunk-TWQQHXVG.js";
71
+ } from "./chunk-AYAN7Q2X.js";
55
72
 
56
73
  // src/tui/index.tsx
57
74
  import { render } from "ink";
@@ -177,10 +194,7 @@ import { spawnSync } from "child_process";
177
194
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
178
195
  import { join } from "path";
179
196
  var EXCERPT_CHARS = 1200;
180
- function tildify(p) {
181
- const home = userHome();
182
- return p.startsWith(home) ? "~" + p.slice(home.length) : p;
183
- }
197
+ var tildify2 = (p) => tildify(p);
184
198
  function skillExcerpt(dir, chars) {
185
199
  const file = join(dir, "SKILL.md");
186
200
  if (!existsSync(file)) return { text: "(SKILL.md not found)", files: 0, size: 0 };
@@ -204,7 +218,7 @@ function buildReviewPrompt(skills, ctx) {
204
218
  `- Description: ${s.description || "(none)"}`,
205
219
  `- Source: ${s.source}`,
206
220
  `- Installed via: ${s.installedHow}`,
207
- `- 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(", ")})` : ""}`,
208
222
  `- Loaded by: ${s.loadedBy}`,
209
223
  ...s.vendor ? [`- Vendor-managed: ${s.vendor}`] : [],
210
224
  ...s.keptGlobal ? ["- I marked this as global on purpose: recommend changing that only for a strong reason (unused, duplicated, low quality)"] : [],
@@ -230,7 +244,7 @@ function buildReviewPrompt(skills, ctx) {
230
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.
231
245
 
232
246
  ## My projects
233
- ${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)"}
234
248
 
235
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).
236
250
 
@@ -281,9 +295,17 @@ function matchScore(text, query) {
281
295
  }
282
296
 
283
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
+ };
284
306
  var SEVERITY_RANK = { problem: 0, warning: 1, hint: 2 };
285
307
  function filterOf(source) {
286
- 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";
287
309
  }
288
310
  function project(w, name) {
289
311
  return w.projects.find((p) => p.name === name);
@@ -299,24 +321,82 @@ function dailyUses(w, name, onlyRepo) {
299
321
  }
300
322
  return days;
301
323
  }
324
+ function machineIn(w, p) {
325
+ return p.machine ?? w.machine;
326
+ }
302
327
  function usable(w, projectName) {
303
328
  const p = project(w, projectName);
304
329
  const uses = (n) => w.usage[projectName]?.[n] ?? 0;
305
330
  return [
306
331
  ...p.skills.map((s) => ({ name: s.name, source: s.source, where: s.dir, agents: s.agents, uses: uses(s.name), local: s })),
307
- ...w.machine.map((m) => ({ name: m.name, source: m.source, where: m.where, agents: m.agents, uses: uses(m.name), machine: m }))
332
+ ...machineIn(w, p).map((m) => ({ name: m.name, source: m.source, where: m.where, agents: m.agents, uses: uses(m.name), machine: m }))
308
333
  ];
309
334
  }
310
335
  function lib(w, name) {
311
336
  return w.library.find((l) => l.name === name);
312
337
  }
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
+ }
343
+ function dupeIssues(repo, name, d) {
344
+ const g = repo === null ? "-g" : "";
345
+ if (d.differ)
346
+ return [
347
+ {
348
+ id: `conflict${g}:${name}`,
349
+ severity: "problem",
350
+ title: `Copies differ: ${d.differ.map((c) => c.label + (c.runs.length ? ` (${agentNames(c.runs)})` : "")).join(" vs ")}`,
351
+ short: "Copies differ",
352
+ decision: true,
353
+ fixes: d.differ.filter((c) => c.canWin).map((c) => ({
354
+ label: `Keep the ${c.label} copy`,
355
+ preview: `The other ${d.differ.length === 2 ? "copy becomes a link" : "copies become links"} to it, so every agent runs this one. Replaced copies go to Settings \u203A Backups.${repo ? " If git tracks a copy that changes, skilllib asks first." : ""}`,
356
+ run: (w) => w.ops.tidy(repo, name, { keep: c.dir })
357
+ }))
358
+ }
359
+ ];
360
+ return [
361
+ {
362
+ id: `copies${g}:${name}`,
363
+ severity: "warning",
364
+ title: repo ? "More copies or links in this repo than your agents need" : "Identical copies in several global folders",
365
+ short: "Extra copies",
366
+ decision: false,
367
+ fixes: [
368
+ {
369
+ label: "Keep one copy, plus the links your agents need",
370
+ preview: `${d.steps.join("; ")}. Replaced copies go to Settings \u203A Backups.${d.git.length ? ` Git would see ${d.git.join(", ")}: skilllib asks before ${d.git.length === 1 ? "that one" : "those"}.` : ""}`,
371
+ run: (w) => w.ops.tidy(repo, name)
372
+ }
373
+ ]
374
+ }
375
+ ];
376
+ }
377
+ function cursorPluginIssue(name, plugin) {
378
+ return {
379
+ id: `cursor-plugin:${name}`,
380
+ severity: "warning",
381
+ title: `Also in the Cursor plugin ${plugin}: Cursor lists both while it's on`,
382
+ short: "Also in a Cursor plugin",
383
+ decision: true,
384
+ fixes: [
385
+ {
386
+ label: "Turn the plugin off in Cursor (Settings \u203A Plugins)",
387
+ preview: "skilllib can't read or change Cursor's plugins. Your skill wins once the plugin is off.",
388
+ run: () => `Turn ${plugin} off in Cursor; skilllib can't tell whether it's on`
389
+ }
390
+ ]
391
+ };
392
+ }
313
393
  function globalFixes(m) {
314
394
  return [
315
395
  {
316
396
  label: "Move it to the repos that need it\u2026",
317
397
  preview: `Pick repos. ${m.name} goes into your library and those repos, and stops loading globally (original backed up).`,
318
398
  run: () => "",
319
- pickRepos: (w, repos) => w.ops.moveGlobal(m, repos)
399
+ pickRepos: (w, repos) => w.ops.moveGlobal([m], repos)
320
400
  },
321
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) },
322
402
  { label: "Delete it", preview: `Remove ${m.path}. A backup is kept in Settings \u203A Backups.`, run: (w) => w.ops.deleteGlobal(m) }
@@ -324,9 +404,10 @@ function globalFixes(m) {
324
404
  }
325
405
  function machineActions(m) {
326
406
  if (m.broken) return [];
327
- return m.source === "global" ? globalFixes(m) : [];
407
+ return m.source === "global" ? globalFixes(m) : m.source === "skilllib" ? [removeAgentSkillFix()] : [];
328
408
  }
329
409
  function machineIssues(w, m) {
410
+ if (m.source === "skilllib") return [agentSkillIssue(w)].flatMap((i) => i ? [i] : []);
330
411
  if (m.broken)
331
412
  return [
332
413
  {
@@ -346,42 +427,64 @@ function machineIssues(w, m) {
346
427
  severity: "problem",
347
428
  title: `Loaded twice: also in ${other.source} ${other.where}`,
348
429
  short: "Loaded twice",
349
- decision: false,
430
+ // Every repo then runs the other copy instead of yours: a decision, never "fix all".
431
+ decision: true,
350
432
  fixes: [
351
433
  {
352
434
  label: `Keep the ${other.source}'s copy, stop loading yours globally`,
353
- 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.`,
354
436
  run: (w2) => w2.ops.unloadGlobal(m)
355
437
  }
356
438
  ]
357
439
  });
358
- const twins = w.machine.filter((x) => x.source === "global" && x.name === m.name && !x.broken && x.agents.some((a) => m.agents.includes(a)));
359
- if (m.source === "global" && twins.length > 1)
440
+ if (m.source === "global" && m.dupes) issues.push(...dupeIssues(null, m.name, m.dupes));
441
+ if (m.source === "global" && m.cursorPlugin && m.agents.includes("cursor")) issues.push(cursorPluginIssue(m.name, m.cursorPlugin));
442
+ const seen = new Set(w.machine.filter((x) => x.name === m.name && !x.broken).flatMap((x) => x.agents));
443
+ const blind = m.source === "global" && m.agents.length && !m.dupes ? w.agents.filter((a) => !seen.has(a)) : [];
444
+ const names = blind.map((a) => harness(a).name).join(", ");
445
+ const link = {
446
+ label: `Link it for ${names}`,
447
+ preview: `Add a link in the global skills folder ${names} read${blind.length === 1 ? "s" : ""} (nothing is copied or moved), so ${names} load${blind.length === 1 ? "s" : ""} it in every repo too.`,
448
+ run: (w2) => w2.ops.linkGlobal(m, blind)
449
+ };
450
+ if (m.kept && blind.length)
451
+ issues.push({ id: `blind-global:${m.name}`, severity: "warning", title: `${names} can't see it: only ${m.agents.map((a) => harness(a).name).join(", ")} load${m.agents.length === 1 ? "s" : ""} it`, short: `${names} can't see it`, decision: false, fixes: [link] });
452
+ if (m.source === "global" && !m.kept) {
453
+ const [move, ...rest] = globalFixes(m);
360
454
  issues.push({
361
- id: `dup-global:${m.name}`,
362
- severity: "problem",
363
- title: `Loaded twice: in ${twins.map((t) => t.where).join(" and ")}`,
364
- short: "Loaded twice",
455
+ id: `global:${m.name}`,
456
+ severity: "warning",
457
+ title: blind.length ? `Global, not reviewed: loads in every repo, but ${names} can't see it` : "Global, not reviewed: loads in every repo",
458
+ short: blind.length ? `Not reviewed \xB7 ${names} can't see it` : "Not reviewed",
365
459
  decision: true,
366
- fixes: twins.map((t) => ({ label: `Delete the copy in ${t.where}`, preview: `Move ${t.path} to Settings \u203A Backups; the other copy stays.`, run: (w2) => w2.ops.deleteGlobal(t) }))
460
+ fixes: blind.length ? [move, link, ...rest] : [move, ...rest]
367
461
  });
368
- if (m.source === "global" && !m.kept)
369
- issues.push({ id: `global:${m.name}`, severity: "warning", title: "Global, not reviewed: loads in every repo", short: "Not reviewed", decision: true, fixes: globalFixes(m) });
462
+ }
370
463
  return issues;
371
464
  }
372
465
  function issuesOf(w, projectName, u) {
373
466
  const s = u.local;
374
467
  const issues = [];
375
468
  const shares = (m) => m.name === s.name && !m.broken && m.agents.some((a) => s.agents.includes(a));
376
- const g = w.machine.find((m) => m.source === "global" && shares(m));
377
- const vendor = w.machine.find((m) => m.source !== "global" && shares(m));
469
+ const loaded = machineIn(w, project(w, projectName));
470
+ const g = loaded.find((m) => m.source === "global" && shares(m));
471
+ const vendor = loaded.find((m) => m.source !== "global" && shares(m));
378
472
  const l = lib(w, s.name);
379
473
  const remove = {
380
474
  label: "Remove this repo's copy",
381
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.`,
382
476
  run: (w2) => w2.ops.remove(projectName, s.name)
383
477
  };
384
- 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)
385
488
  issues.push({
386
489
  id: `missing:${s.name}`,
387
490
  severity: "problem",
@@ -390,7 +493,7 @@ function issuesOf(w, projectName, u) {
390
493
  decision: false,
391
494
  fixes: [{ label: "Restore it (sync)", preview: `Reinstall ${s.name} v${s.version} into ${s.dir}.`, run: (w2) => w2.ops.restore(projectName, s.name) }]
392
495
  });
393
- if (s.source === "lib" && s.library === "missing")
496
+ else if (s.source === "lib" && s.library === "missing")
394
497
  issues.push({
395
498
  id: `not-in-lib:${s.name}`,
396
499
  severity: "problem",
@@ -399,23 +502,50 @@ function issuesOf(w, projectName, u) {
399
502
  decision: false,
400
503
  fixes: [{ label: "Copy it back into your library", preview: `Copy this repo's ${s.name} into your library, so it can be restored and shared again.`, run: (w2) => w2.ops.copyToLibrary(projectName, s.name) }]
401
504
  });
402
- if (g)
505
+ if (g?.kept)
506
+ issues.push({
507
+ id: `kept-g:${s.name}`,
508
+ severity: "warning",
509
+ title: `Also global in ${g.where}, and you keep it global: this repo's copy is extra`,
510
+ short: "Extra: you keep it global",
511
+ decision: true,
512
+ fixes: [
513
+ ...s.source !== "repo" ? [{ ...remove, label: "Remove this repo's copy, keep it global" }] : [],
514
+ {
515
+ label: "Stop loading it globally after all",
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)}.`,
517
+ run: (w2) => w2.ops.unloadGlobal(g)
518
+ }
519
+ ]
520
+ });
521
+ else if (g)
403
522
  issues.push({
404
523
  id: `twice-g:${s.name}`,
405
524
  severity: "problem",
406
525
  title: `Loaded twice: also global in ${g.where}`,
407
526
  short: "Loaded twice",
408
- decision: false,
527
+ // Unloading the global copy changes every repo: a decision, never "fix all".
528
+ decision: true,
409
529
  fixes: [
410
530
  {
411
531
  label: "Keep this repo's copy, stop loading it globally",
412
- 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)}.`,
413
533
  run: (w2) => w2.ops.unloadGlobal(g)
414
534
  },
415
535
  ...s.source !== "repo" ? [{ ...remove, label: "Keep it global, remove it from this repo" }] : []
416
536
  ]
417
537
  });
418
- 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)
419
549
  issues.push({
420
550
  id: `twice-p:${s.name}`,
421
551
  severity: "problem",
@@ -424,13 +554,19 @@ function issuesOf(w, projectName, u) {
424
554
  decision: true,
425
555
  fixes: [
426
556
  ...s.source !== "repo" ? [{ ...remove, label: `Remove this repo's copy, use the ${vendor.source}'s` }] : [],
427
- {
428
- label: vendor.source === "plugin" ? "Turn the plugin off with /plugin in Claude Code" : `Turn it off in ${vendor.where}`,
429
- preview: "skilllib can't turn it off for you yet.",
557
+ vendor.source === "plugin" ? {
558
+ label: `Turn the plugin off in ${projectName} only`,
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." : ""}`,
560
+ run: (w2) => w2.ops.pluginOffHere(projectName, vendor.where)
561
+ } : {
562
+ label: `Turn it off in ${vendor.where}`,
563
+ preview: "skilllib can't turn it off for you.",
430
564
  run: () => `Turn ${vendor.where} off at its source; skilllib picks it up next time`
431
565
  }
432
566
  ]
433
567
  });
568
+ if (s.dupes) issues.push(...dupeIssues(projectName, s.name, s.dupes));
569
+ if (s.cursorPlugin && s.agents.includes("cursor")) issues.push(cursorPluginIssue(s.name, s.cursorPlugin));
434
570
  if (s.source === "lib" && s.edited)
435
571
  issues.push({
436
572
  id: `edited:${s.name}`,
@@ -453,22 +589,36 @@ function issuesOf(w, projectName, u) {
453
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) }]
454
590
  });
455
591
  const blind = w.agents.filter((a) => !u.agents.includes(a));
456
- 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)
457
596
  issues.push({
458
597
  id: `blind:${s.name}`,
459
598
  severity: "warning",
460
- title: `${blind.map((a) => harness(a).name).join(", ")} can't see it`,
461
- 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`,
462
601
  decision: false,
463
602
  fixes: [
464
603
  {
465
604
  label: "Link it for every agent",
466
- 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}`,
467
606
  run: (w2) => w2.ops.link(projectName, s.name)
468
607
  }
469
608
  ]
470
609
  });
471
- if (s.source !== "lib" && s.library === "differs")
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
+ });
620
+ const differ = !!s.dupes?.differ;
621
+ if (s.source !== "lib" && s.library === "differs" && !differ)
472
622
  issues.push({
473
623
  id: `differs:${s.name}`,
474
624
  severity: "hint",
@@ -480,22 +630,24 @@ function issuesOf(w, projectName, u) {
480
630
  ...s.source === "untracked" ? [{ label: "Replace it with the library version", preview: `Replace ${s.dir}/${s.name} with your library's newest version and track it (this copy backed up).`, run: (w2) => w2.ops.discardEdits(projectName, s.name) }] : []
481
631
  ]
482
632
  });
483
- if (s.source === "untracked" && s.library === "same")
633
+ if (s.source === "untracked" && s.library === "same" && !differ)
484
634
  issues.push({
485
635
  id: `adopt:${s.name}`,
486
636
  severity: "hint",
487
637
  title: "Same as your library skill, but not tracked",
488
638
  short: "Not tracked",
489
- 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,
490
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) }]
491
642
  });
492
- if (s.source === "untracked" && !l && !vendor)
643
+ if (s.source === "untracked" && !l && !vendor && !differ)
493
644
  issues.push({
494
645
  id: `local:${s.name}`,
495
646
  severity: "hint",
496
647
  title: "Only exists in this repo",
497
648
  short: "Only in this repo",
498
- 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,
499
651
  fixes: [
500
652
  {
501
653
  label: "Import it into your library",
@@ -504,7 +656,7 @@ function issuesOf(w, projectName, u) {
504
656
  }
505
657
  ]
506
658
  });
507
- 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)
508
660
  issues.push({
509
661
  id: `unused:${s.name}`,
510
662
  severity: "hint",
@@ -515,6 +667,53 @@ function issuesOf(w, projectName, u) {
515
667
  });
516
668
  return issues;
517
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
+ }
518
717
  function worst(issues) {
519
718
  return [...issues].sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity])[0];
520
719
  }
@@ -543,7 +742,7 @@ function deleteLibraryFix(w, names) {
543
742
  "Every copy goes to Settings \u203A Backups.",
544
743
  others.length ? `Untracked or the team's own copies in ${others.join(", ")} stay.` : ""
545
744
  ].filter(Boolean).join(" "),
546
- 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)))
547
746
  };
548
747
  }
549
748
  function replacePluginFix(w, id) {
@@ -589,6 +788,8 @@ function groupCandidates(w, u) {
589
788
  if (m?.source === "plugin") return [{ key: `plugin:${m.where}`, label: `\u29C9 ${m.where.split("@")[0]} plugin` }];
590
789
  if (m?.source === "claude.ai") return [{ key: "claude.ai", label: "claude.ai skills" }];
591
790
  if (m?.source === "cursor") return [{ key: "cursor", label: "Cursor built-ins" }];
791
+ if (m?.source === "system") return [{ key: `system:${m.where}`, label: m.where }];
792
+ if (m?.source === "skilllib") return [];
592
793
  if (m) {
593
794
  const origin = originGroup(m.origin);
594
795
  return [...origin ? [origin] : [], ...byTime("global", m.installed), ...byWord("global", m.name)];
@@ -632,10 +833,8 @@ function addRows(w, target, q) {
632
833
  name: q ? `+ Create "${q}"${where}` : "+ Create a new skill (type its name)",
633
834
  note: "",
634
835
  description: "",
635
- run: (w2) => {
636
- const made = w2.ops.createSkill(q);
637
- return target ? w2.ops.add(target, q) : made;
638
- }
836
+ run: (w2) => w2.ops.createSkill(q),
837
+ ...target ? { afterEdit: (w2) => w2.ops.add(target, q) } : {}
639
838
  },
640
839
  { key: "agent", agent: true, name: q ? `\u2726 Write "${q}" with an agent` : "\u2726 Write a new skill with an agent", note: "", description: "" }
641
840
  ];
@@ -644,9 +843,32 @@ function addRows(w, target, q) {
644
843
  }
645
844
 
646
845
  // src/tui/prompts.ts
647
- 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) {
648
850
  const skills = names.map((name) => {
649
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
+ }
650
872
  const m = copies.find((c) => c.source === "global") ?? copies[0];
651
873
  const lib2 = w.library.find((l) => l.name === name);
652
874
  const repos = w.projects.filter((p) => p.skills.some((s) => s.name === name));
@@ -654,11 +876,11 @@ function reviewPrompt(w, names, scope) {
654
876
  return {
655
877
  name,
656
878
  description: w.descriptions[name] ?? "",
657
- source: m ? m.source === "global" ? "my global skills" : `vendor: ${m.source} (${m.where})` : lib2 ? "Your skills (skilllib library)" : "a repo's own skill",
658
- 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"),
659
881
  path: m?.path ?? (lib2 ? w.ops.libraryFile(name).replace(/\/SKILL\.md$/, "") : local?.path ?? ""),
660
882
  links: m?.links ?? [],
661
- 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",
662
884
  vendor: m && m.source !== "global" ? "turn it off at its source (/plugin in Claude Code, claude.ai settings, or Cursor)" : null,
663
885
  usesTotal: w.days ? totalUses(w, name) : null,
664
886
  usesByProject: Object.entries(w.usage).flatMap(([p, u]) => u[name] ? [[p, u[name]]] : []),
@@ -691,9 +913,7 @@ Keep it under 200 lines. ${p ? "skilllib will offer to import it into my library
691
913
 
692
914
  // src/tui/App.tsx
693
915
  import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
694
- function tildify2(p) {
695
- return p.startsWith(userHome()) ? "~" + p.slice(userHome().length) : p;
696
- }
916
+ var tildify3 = (p) => tildify(p);
697
917
  var PLACES = [
698
918
  ["projects", "Projects"],
699
919
  ["library", "Your skills"],
@@ -725,6 +945,7 @@ function tag(u, w) {
725
945
  if (s) return [s.source, color.blue];
726
946
  if (u.source === "global") return [u.machine?.kept ? "global \u2713" : "global", color.yellow];
727
947
  if (u.source === "plugin") return [`\u29C9 ${u.where.split("@")[0]}`, color.magenta];
948
+ if (u.source === "skilllib") return ["\u25C6 skilllib", color.accent];
728
949
  return [u.source, color.muted];
729
950
  }
730
951
  function isNameChar(input, key) {
@@ -775,7 +996,7 @@ function TitleRow({ cells, width }) {
775
996
  }
776
997
  var fixOption = (fix) => ({ label: fix.label, fix });
777
998
  var GIT_LABEL = { committed: "\u2713 committed", changed: "\xB1 uncommitted changes", new: "+ not committed", ignored: "\u2205 gitignored" };
778
- var COPY_RANK = { lib: 0, repo: 0, untracked: 0, global: 1, plugin: 2, "claude.ai": 3, cursor: 3 };
999
+ var COPY_RANK = { lib: 0, repo: 0, untracked: 0, global: 1, skilllib: 1, plugin: 2, "claude.ai": 3, cursor: 3, system: 3 };
779
1000
  function byName(copies) {
780
1001
  const groups = /* @__PURE__ */ new Map();
781
1002
  for (const c of copies) groups.set(c.name, [...groups.get(c.name) ?? [], c]);
@@ -843,7 +1064,7 @@ function skillItem(w, copies, issues, nameW, actions, repo) {
843
1064
  const b = builder(u.name);
844
1065
  for (const l of wrap(w.descriptions[u.name] || "(no description)", width - 4, 3)) b.line([l, color.muted]);
845
1066
  b.line();
846
- 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]);
847
1068
  else b.issues(issues);
848
1069
  b.actions(actions, width);
849
1070
  b.line();
@@ -852,7 +1073,7 @@ function skillItem(w, copies, issues, nameW, actions, repo) {
852
1073
  b.line(["Loaded from", color.accent]);
853
1074
  for (const c of copies) {
854
1075
  const s = c.local;
855
- 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;
856
1077
  const extra = s ? s.missing ? "missing" : s.git ? GIT_LABEL[s.git] : "" : "";
857
1078
  const [ct, cc] = c === u ? ["", void 0] : tag(c, w);
858
1079
  b.line([" ", void 0], [path + " ", color.text], [ct ? ct + " " : "", cc], [extra, color.faint]);
@@ -866,7 +1087,10 @@ function projectSkillItems(w, p, nameW, ui) {
866
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)
867
1088
  );
868
1089
  }
869
- 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
+ });
870
1094
  function versionsOption(w, repo, name, installed, ui) {
871
1095
  return {
872
1096
  label: "Other versions\u2026",
@@ -879,27 +1103,35 @@ function versionsOption(w, repo, name, installed, ui) {
879
1103
  function skillActions(w, p, u, ui) {
880
1104
  const s = u.local;
881
1105
  const inLibrary = w.library.find((l) => l.name === u.name);
882
- 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
+ };
883
1112
  if (s?.source === "lib")
884
1113
  return [
885
- ...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) })] : [],
886
- { label: "Edit in library", action: () => ui.edit(w.ops.libraryFile(u.name)) },
887
- 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
+ ] : [],
888
1120
  fixOption(remove),
889
- reviewOption(w, u.name, ui)
1121
+ reviewOption(w, u.name, ui, p)
890
1122
  ];
891
1123
  if (s?.source === "repo")
892
1124
  return [
893
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) })],
894
1126
  { label: "Edit SKILL.md", action: () => ui.edit(`${s.path}/SKILL.md`) },
895
- reviewOption(w, u.name, ui)
1127
+ reviewOption(w, u.name, ui, p)
896
1128
  ];
897
1129
  if (s)
898
1130
  return [
899
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) }),
900
1132
  { label: "Edit SKILL.md", action: () => ui.edit(`${s.path}/SKILL.md`) },
901
1133
  fixOption(remove),
902
- reviewOption(w, u.name, ui)
1134
+ reviewOption(w, u.name, ui, p)
903
1135
  ];
904
1136
  return [{ label: "Open in Global", action: () => ui.openInGlobal(u.name) }, reviewOption(w, u.name, ui)];
905
1137
  }
@@ -908,7 +1140,7 @@ function overview(w, p) {
908
1140
  const info = w.ops.repoInfo(p);
909
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];
910
1142
  return [
911
- ["Folder", [[tildify2(project2.path), color.text]]],
1143
+ ["Folder", [[tildify3(project2.path), color.text]]],
912
1144
  ["Remote", [[info.remote ?? "none", info.remote ? color.blue : color.faint]]],
913
1145
  ["Branch", [[info.branch ?? "\u2013", color.text], [info.dirty ? ` ${info.dirty} uncommitted` : "", color.yellow]]],
914
1146
  ["Manifest", [manifest]]
@@ -955,11 +1187,11 @@ function globalItems(w, nameW, ui) {
955
1187
  }
956
1188
  function globalActions(w, u, ui) {
957
1189
  const m = u.machine;
958
- 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" : "Cursor manages it";
1190
+ const atSource = m.source === "plugin" ? "Turn it off with /plugin in Claude Code" : m.source === "claude.ai" ? "Turn it off in claude.ai \u203A Settings" : m.source === "system" ? `An admin manages ${m.where}` : "Cursor manages it";
959
1191
  return [
960
1192
  ...machineActions(m).map(fixOption),
961
1193
  ...m.source === "plugin" ? [fixOption(replacePluginFix(w, m.where))] : [],
962
- ...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`) }],
963
1195
  ...m.source === "global" && !m.broken ? [{ label: "Edit SKILL.md", action: () => ui.edit(`${m.path}/SKILL.md`) }] : [],
964
1196
  reviewOption(w, m.name, ui)
965
1197
  ];
@@ -967,8 +1199,9 @@ function globalActions(w, u, ui) {
967
1199
  function libraryItems(w, nameW, ui) {
968
1200
  return w.library.map((l) => {
969
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"));
970
1203
  const uses = totalUses(w, l.name);
971
- 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]) };
972
1205
  const delFix = deleteLibraryFix(w, [l.name]);
973
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] }];
974
1207
  const behind = repos.filter((p) => p.skills.some((s) => s.name === l.name && s.source === "lib" && s.version < l.latest)).length;
@@ -1000,13 +1233,13 @@ function libraryItems(w, nameW, ui) {
1000
1233
  b.issues(issues);
1001
1234
  b.actions([
1002
1235
  fixOption(addFix),
1003
- ...repos.length ? [
1236
+ ...removable.length ? [
1004
1237
  fixOption({
1005
1238
  label: "Remove from repos\u2026",
1006
1239
  preview: `Pick repos to remove ${l.name} from. Your library keeps it.`,
1007
1240
  run: () => "",
1008
- candidates: () => repos.map((p) => p.name),
1009
- 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)))
1010
1243
  })
1011
1244
  ] : [],
1012
1245
  fixOption(delFix),
@@ -1022,15 +1255,39 @@ function libraryItems(w, nameW, ui) {
1022
1255
  }
1023
1256
  function healthItems(w, nameW, ui) {
1024
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]] }));
1025
- 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
+ ];
1026
1285
  }
1027
1286
  function addCursor(rows, cursor) {
1028
1287
  const i = Math.min(cursor, rows.length - 1);
1029
1288
  return rows[i] && !rows[i].header ? i : Math.max(0, rows.findIndex((r) => !r.header));
1030
1289
  }
1031
- function said(r) {
1032
- return typeof r === "string" ? r : r.message;
1033
- }
1290
+ var said2 = said;
1034
1291
  function App({ initial, reload, loadUsage: loadUsage2 }) {
1035
1292
  const { exit, suspendTerminal } = useApp();
1036
1293
  const { columns: termCols, rows: termRows } = useWindowSize();
@@ -1068,12 +1325,14 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1068
1325
  notify: setToast,
1069
1326
  openInGlobal,
1070
1327
  edit,
1071
- copy: (text, what) => setToast(said(world.ops.copy(text, what))),
1328
+ copy: (text, what) => setToast(said2(world.ops.copy(text, what))),
1072
1329
  menu: (title, options) => setModal({ kind: "menu", title, options, cursor: 0 })
1073
1330
  };
1074
1331
  const flagged = [
1075
1332
  ...world.projects.flatMap((p) => projectIssues(world, p.name).map((x) => ({ key: `${p.name}:${x.skill.name}`, issue: x.issue }))),
1076
- ...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 }] : [])
1077
1336
  ];
1078
1337
  const healthCount = new Set(flagged.map((f) => f.key)).size;
1079
1338
  const healthWorst = worst(flagged.map((f) => f.issue));
@@ -1102,16 +1361,18 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1102
1361
  if (items[cursor]?.header) cursor = firstSelectable;
1103
1362
  const current = items[cursor];
1104
1363
  const detail = current?.detail?.(columns);
1105
- function apply(run) {
1364
+ function apply(run, w = world) {
1106
1365
  let result;
1107
1366
  try {
1108
- result = run(world);
1367
+ result = run(w);
1109
1368
  } catch (e) {
1110
- result = `Couldn't do it: ${e.message}`;
1369
+ result = failed(`Couldn't do it: ${e.message}`);
1111
1370
  }
1112
1371
  setBase(reload());
1113
- setToast(`\u2713 ${said(result)}`);
1114
- 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 });
1115
1376
  }
1116
1377
  function choose(o) {
1117
1378
  if (o.action) return o.action();
@@ -1131,6 +1392,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1131
1392
  function fixSelected() {
1132
1393
  const issue = current && worst(current.issues);
1133
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}`);
1134
1396
  if (issue.decision) {
1135
1397
  setFocus("detail");
1136
1398
  setDetailCursor(Math.max(0, detail?.options.findIndex((o) => o.issue === issue) ?? 0));
@@ -1207,7 +1469,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1207
1469
  preview: `Pick repos. These ${n(mine.length)} go into your library and those repos, and stop loading globally (originals backed up).`,
1208
1470
  run: () => "",
1209
1471
  preticked: usedIn,
1210
- 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)
1211
1473
  }),
1212
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`) })] : [],
1213
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)`) })
@@ -1218,7 +1480,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1218
1480
  preview: `Install these ${n(libs.length)} into the repos you pick.`,
1219
1481
  run: () => "",
1220
1482
  preticked: () => [],
1221
- 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))
1222
1484
  })
1223
1485
  ] : [],
1224
1486
  ...libs.length ? [fixOption(deleteLibraryFix(world, libs.map((l) => l.name)))] : [],
@@ -1258,9 +1520,9 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1258
1520
  `\u22EF ${repo}\u2026`,
1259
1521
  ["Open its folder, hide it from the list, or copy a review prompt for its skills."],
1260
1522
  () => ui.menu(repo, [
1261
- { 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))) },
1262
1524
  { label: "Hide it from the list", action: () => (apply((w) => w.ops.hide(repo)), setOpen(null), setFocus(sidebar ? "sidebar" : "list")) },
1263
- ...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}`) }] : []
1264
1526
  ])
1265
1527
  )
1266
1528
  ];
@@ -1292,19 +1554,21 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1292
1554
  setQuery("");
1293
1555
  setChip("all");
1294
1556
  }
1295
- function edit(file) {
1296
- 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`));
1297
1559
  const editor = process.env.VISUAL || process.env.EDITOR || (process.platform === "win32" ? "notepad" : "vi");
1298
1560
  void suspendTerminal(() => {
1299
1561
  if (process.platform === "win32") spawnSync2(`${editor} "${file}"`, { shell: true, stdio: "inherit" });
1300
1562
  else spawnSync2("/bin/sh", ["-c", `${editor} "$1"`, "sh", file], { stdio: "inherit" });
1301
1563
  }).then(() => {
1302
- setBase(reload());
1303
- setToast(`Saved ${tildify2(file)}`);
1564
+ const fresh = reload();
1565
+ if (then) return apply(then, fresh);
1566
+ setBase(fresh);
1567
+ setToast(`Saved ${tildify3(file)}`);
1304
1568
  });
1305
1569
  }
1306
1570
  function askForFolder(title) {
1307
- 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)) });
1308
1572
  }
1309
1573
  useEffect(() => {
1310
1574
  const folder = () => !initial.roots.length && askForFolder("Where are your projects? skilllib finds the git repos in there");
@@ -1333,20 +1597,27 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1333
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"}`));
1334
1598
  })
1335
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
+ ),
1336
1607
  header("#roots", "Project folders"),
1337
1608
  ...world.roots.map(
1338
1609
  (r) => row(
1339
1610
  `root:${r}`,
1340
- tildify2(r),
1611
+ tildify3(r),
1341
1612
  "scanned for git repos",
1342
- () => ui.menu(tildify2(r), [
1613
+ () => ui.menu(tildify3(r), [
1343
1614
  { label: "Look for repos again", action: () => apply((w) => w.ops.rescan()) },
1344
- 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) })
1345
1616
  ])
1346
1617
  )
1347
1618
  ),
1348
1619
  row("root:add", "+ Add a folder\u2026", "", () => askForFolder("Folder that contains your projects")),
1349
- ...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))))] : [],
1350
1621
  header("#backups", "Backups"),
1351
1622
  ...world.backups.length ? world.backups.map(
1352
1623
  (b, i) => row(`backup:${i}`, `\u21BA ${b.name}`, `${b.from} \xB7 ${b.at}`, () => apply((w) => w.ops.restoreBackup(i)))
@@ -1450,7 +1721,11 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1450
1721
  }
1451
1722
  if (m.kind === "input") {
1452
1723
  if (key.escape) return setModal(null);
1453
- 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
+ }
1454
1729
  if (key.backspace || key.delete) return setModal({ ...m, value: m.value.slice(0, -1) });
1455
1730
  if (key.ctrl && input === "u") return setModal({ ...m, value: "" });
1456
1731
  if (input && !key.ctrl && !key.meta) return setModal({ ...m, value: m.value + input });
@@ -1463,7 +1738,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1463
1738
  if (key.return && m.options[m.cursor]) return setModal(null), choose(m.options[m.cursor]);
1464
1739
  return;
1465
1740
  }
1466
- if (key.escape) return setModal(null);
1741
+ if (key.escape) return setModal(null), m.kind === "confirm" && m.toast && setToast(m.toast);
1467
1742
  if (m.kind === "confirm") {
1468
1743
  if (key.return || input === "y") {
1469
1744
  setModal(null);
@@ -1500,17 +1775,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1500
1775
  if (key.return) {
1501
1776
  setModal(null);
1502
1777
  const chosen = m.items.filter((it) => it.on);
1503
- apply((w) => {
1504
- let ok = 0;
1505
- for (const it of chosen) {
1506
- try {
1507
- it.fix.run(w);
1508
- ok++;
1509
- } catch {
1510
- }
1511
- }
1512
- return `Applied ${ok} fix${ok === 1 ? "" : "es"} (anything removed is in Settings \u203A Backups)`;
1513
- });
1778
+ apply((w) => runAll(w, chosen.map((it) => it.fix)));
1514
1779
  }
1515
1780
  return;
1516
1781
  }
@@ -1533,9 +1798,9 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1533
1798
  const row = rows[cursor2];
1534
1799
  if (row.create && !/^[a-z0-9]/.test(m.query)) return setToast("Type the new skill's name first");
1535
1800
  setModal(null);
1536
- apply(row.run);
1537
- if (row.create) edit(world.ops.libraryFile(m.query));
1538
- return;
1801
+ if (!row.create) return apply(row.run);
1802
+ row.run(world);
1803
+ return edit(world.ops.libraryFile(m.query), row.afterEdit);
1539
1804
  }
1540
1805
  if (isNameChar(input, key)) return setModal({ ...m, query: m.query + input.toLowerCase(), cursor: 0 });
1541
1806
  }
@@ -1648,8 +1913,11 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1648
1913
  );
1649
1914
  else if (modal?.kind === "input")
1650
1915
  body = /* @__PURE__ */ jsxs2(Panel, { title: modal.title, focused: true, width: columns, height: 5, children: [
1651
- /* @__PURE__ */ jsx2(Text2, { color: color.text, children: modal.value + "\u258F" }),
1652
- /* @__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` })
1653
1921
  ] });
1654
1922
  else if (modal?.kind === "agents")
1655
1923
  body = /* @__PURE__ */ jsx2(
@@ -1776,7 +2044,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1776
2044
  ] })
1777
2045
  ] }),
1778
2046
  /* @__PURE__ */ jsxs2(Box2, { height: 1, width: termCols, justifyContent: "space-between", children: [
1779
- /* @__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 }),
1780
2048
  toast ? null : /* @__PURE__ */ jsx2(Text2, { color: color.faint, children: hints + " " })
1781
2049
  ] })
1782
2050
  ] });
@@ -1835,9 +2103,8 @@ function Help({ width, height }) {
1835
2103
  import { execFileSync, spawn, spawnSync as spawnSync3 } from "child_process";
1836
2104
  import { existsSync as existsSync3, lstatSync, realpathSync } from "fs";
1837
2105
  import { basename, dirname, join as join2 } from "path";
1838
- function tildify3(p) {
1839
- const home = userHome();
1840
- return (p.startsWith(home) ? "~" + p.slice(home.length) : p).replace(/\\/g, "/");
2106
+ function tildify4(p) {
2107
+ return tildify(p).replace(/\\/g, "/");
1841
2108
  }
1842
2109
  function uniqueNames(roots) {
1843
2110
  const tail = (r, n) => r.split(/[\\/]/).filter(Boolean).slice(-n).join("/");
@@ -1855,25 +2122,37 @@ function localSkill(root, s, git2) {
1855
2122
  "update available": {},
1856
2123
  "edited locally": { edited: true },
1857
2124
  "edited locally, update available": { edited: true },
1858
- "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" } },
1859
2127
  "not in library": { library: "missing" }
1860
2128
  };
1861
2129
  const other = {
1862
2130
  "untracked copy of library skill": { source: "untracked", library: "same" },
1863
2131
  "untracked, differs from library": { source: "untracked", library: "differs" },
1864
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" },
1865
2135
  "repo skill": { source: "repo" },
1866
2136
  "repo skill, in library": { source: "repo", library: "same" },
1867
2137
  "repo skill, differs from library": { source: "repo", library: "differs" }
1868
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]));
1869
2144
  return {
1870
2145
  name: s.name,
1871
2146
  source: "lib",
1872
2147
  dir: s.location,
1873
2148
  path: s.path,
1874
- agents: s.visibility.map((v) => v.id),
1875
- ...s.managed ? { library: "same", version: s.version ?? void 0, ...managed[s.state] } : other[s.state],
1876
- 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
1877
2156
  };
1878
2157
  }
1879
2158
  var partsOf = /* @__PURE__ */ new Map();
@@ -1903,8 +2182,8 @@ function machineSkill(s, kept) {
1903
2182
  const global = s.kind === "global" || s.kind === "skills.sh";
1904
2183
  return {
1905
2184
  name: s.name,
1906
- source: global ? "global" : s.kind === "plugin" ? "plugin" : s.kind === "claude.ai" ? "claude.ai" : "cursor",
1907
- 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,
1908
2187
  ...s.kind === "skills.sh" ? { origin: `npx skills: ${s.origin}` } : {},
1909
2188
  path: s.path,
1910
2189
  links: s.links,
@@ -1915,11 +2194,17 @@ function machineSkill(s, kept) {
1915
2194
  ...s.kind === "plugin" ? { pluginParts: pluginParts(dirname(dirname(s.path))) } : {}
1916
2195
  };
1917
2196
  }
2197
+ function dupesOf(r, root, git2 = null) {
2198
+ const out = /* @__PURE__ */ new Map();
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)) });
2200
+ for (const c of r.conflicts)
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 })) });
2202
+ return out;
2203
+ }
1918
2204
  function loadWorld() {
1919
2205
  forgetLatest();
1920
2206
  const agents = enabledHarnesses();
1921
- const cwdRoot = findProjectRoot();
1922
- const here = isProjectCandidate(cwdRoot) && (existsSync3(join2(cwdRoot, ".git")) || existsSync3(join2(cwdRoot, "skilllib.json"))) ? cwdRoot : null;
2207
+ const here = projectHere();
1923
2208
  const real = (p) => {
1924
2209
  try {
1925
2210
  return realpathSync(p);
@@ -1938,20 +2223,39 @@ function loadWorld() {
1938
2223
  return { name: s.name, latest: latestVersion(s.name)?.version ?? 1, origin: origins[s.name] };
1939
2224
  });
1940
2225
  const kept = keptGlobal();
1941
- const machine = machineSkills(agents).map((s) => {
2226
+ const cursorPlugin = new Map(agents.includes("cursor") ? cursorPluginSkills().map((s) => [s.name, s.plugin]) : []);
2227
+ const globalDupes = dupesOf(planGlobalTidy({ enabled: agents }), null);
2228
+ const sourced = machineSkills(agents);
2229
+ const machine = sourced.map((s) => {
1942
2230
  descriptions[s.name] ??= s.description;
1943
- return machineSkill(s, kept);
2231
+ const m = machineSkill(s, kept);
2232
+ if (m.source !== "global" || m.broken) return m;
2233
+ const dupes = globalDupes.get(m.name);
2234
+ const plugin = cursorPlugin.get(m.name);
2235
+ return { ...m, ...dupes ? { dupes } : {}, ...plugin ? { cursorPlugin: plugin } : {} };
1944
2236
  });
2237
+ const plugins = (root) => {
2238
+ const loaded = skillsLoadedIn(root, sourced, agents);
2239
+ if (loaded === sourced) return {};
2240
+ for (const s of loaded) descriptions[s.name] ??= s.description;
2241
+ return { machine: loaded.map((s) => machine[sourced.indexOf(s)] ?? machineSkill(s, kept)) };
2242
+ };
1945
2243
  const projects = roots.map((root) => {
1946
2244
  const status = projectStatus(root);
1947
2245
  const hasManifest = existsSync3(join2(root, "skilllib.json"));
1948
2246
  const git2 = status.length || hasManifest ? gitInfo(root) : null;
1949
2247
  for (const s of status) if (!descriptions[s.name] && !s.state.includes("missing")) descriptions[s.name] = readSkillInfo(s.path).description;
2248
+ const dupes = status.length ? dupesOf(planProjectTidy(root, { enabled: agents }), root, git2) : /* @__PURE__ */ new Map();
1950
2249
  return {
1951
2250
  name: names.get(root),
1952
2251
  path: root,
1953
2252
  manifest: hasManifest ? git2 ? git2.of("skilllib.json") : "no git" : "none",
1954
- skills: status.map((s) => localSkill(root, s, git2))
2253
+ skills: status.map((s) => {
2254
+ const d = dupes.get(s.name);
2255
+ const plugin = cursorPlugin.get(s.name);
2256
+ return { ...localSkill(root, s, git2), ...d ? { dupes: d } : {}, ...plugin ? { cursorPlugin: plugin } : {} };
2257
+ }),
2258
+ ...plugins(root)
1955
2259
  };
1956
2260
  });
1957
2261
  const backups = [...listBackups(), ...pluginBackups()].sort((a, b) => b.movedAt.localeCompare(a.movedAt));
@@ -1963,10 +2267,11 @@ function loadWorld() {
1963
2267
  library,
1964
2268
  usage: {},
1965
2269
  descriptions,
1966
- 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 })),
1967
2271
  roots: readConfig().roots,
1968
2272
  hidden: readConfig().hidden,
1969
2273
  agentsChosen: harnessesChosen(),
2274
+ agentSkill: agentSkillState(agents),
1970
2275
  ops: realOps(new Map(projects.map((p) => [p.name, p.path])), backups)
1971
2276
  };
1972
2277
  }
@@ -1998,44 +2303,99 @@ function remoteLabel(url) {
1998
2303
  if (!url) return void 0;
1999
2304
  return url.replace(/^[a-z+]+:\/\//, "").replace(/^[^@/]+@/, "").replace(/:(?!\d)/, "/").replace(/\.git$/, "");
2000
2305
  }
2001
- function said2(c, repo) {
2306
+ function said3(c, repo) {
2002
2307
  if (c.action === "skipped") return `${c.name}: ${c.reason ?? "skipped"}`;
2003
2308
  if (c.action === "removed") return `Removed ${c.name} from ${repo}`;
2004
- 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;
2005
2312
  }
2006
2313
  function realOps(roots, backups) {
2007
2314
  const rootOf = (repo) => roots.get(repo);
2008
2315
  const find = (repo, name) => projectStatus(rootOf(repo)).find((s) => s.name === name);
2009
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;
2010
2319
  const withLinks = (c, repo, name) => {
2011
- if (!c.blocked?.length) return said2(c, repo);
2012
- 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 }]) };
2013
2325
  };
2014
- const allowTracked = (repo, name, blocked) => ({
2015
- label: `Also link it in ${blocked.join(", ")}`,
2016
- preview: `${blocked.join(", ")} is committed in ${repo}: the links will show in git status. skilllib remembers this for ${repo}.`,
2017
- run: (w) => {
2018
- allowTrackedLinks(rootOf(repo));
2019
- return w.ops.link(repo, name, true);
2020
- }
2021
- });
2022
- 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);
2023
2381
  const toLibrary = (path) => {
2024
2382
  const r = importSkill(path, { force: true });
2025
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`;
2026
2384
  };
2027
2385
  return {
2028
- add: (repo, name) => withLinks(addSkill(rootOf(repo), name), repo, name),
2386
+ add: (repo, name) => addTo([repo], [name]),
2387
+ addTo,
2029
2388
  remove: (repo, name) => {
2030
2389
  const s = find(repo, name);
2031
- if (!s) return `${name} isn't in ${repo}`;
2032
- 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`);
2033
2393
  if (!s.managed) return removeUntracked(rootOf(repo), name, s.path), `Removed ${name} from ${repo} (in Settings \u203A Backups)`;
2034
2394
  return orForce(removeSkill(rootOf(repo), name), repo, "Remove it anyway", () => removeSkill(rootOf(repo), name, { force: true }));
2035
2395
  },
2036
2396
  restore: (repo, name) => {
2037
2397
  const c = syncProject(rootOf(repo)).find((x) => x.name === name);
2038
- return c ? said2(c, repo) : `${name} is already in place`;
2398
+ return c ? result(c, repo) : `${name} is already in place`;
2039
2399
  },
2040
2400
  update: (repo, name) => {
2041
2401
  const c = updateProject(rootOf(repo), [name])[0];
@@ -2053,10 +2413,10 @@ function realOps(roots, backups) {
2053
2413
  return withLinks(addSkill(rootOf(repo), name, s.managed && s.version ? { version: s.version } : {}), repo, name);
2054
2414
  },
2055
2415
  link: (repo, name, allow = false) => {
2056
- const s = find(repo, name);
2057
- const r = s.managed ? relinkDependency(rootOf(repo), name, { allowTracked: allow }) : linkEverywhere(rootOf(repo), name, s.location, { allowTracked: allow });
2058
- const message = r.created.length ? `${name}: linked in ${r.created.join(", ")}` : `${name}: nothing to link`;
2059
- 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 }]) };
2060
2420
  },
2061
2421
  track: (repo, name) => withLinks(addSkill(rootOf(repo), name), repo, name),
2062
2422
  importLocal: (repo, name) => {
@@ -2065,41 +2425,109 @@ function realOps(roots, backups) {
2065
2425
  return withLinks(addSkill(rootOf(repo), name), repo, name);
2066
2426
  },
2067
2427
  copyToLibrary: (repo, name) => toLibrary(find(repo, name).path),
2428
+ tidy: (repo, name, { keep, allowGit = false } = {}) => {
2429
+ const root = repo === null ? null : rootOf(repo);
2430
+ const opts = keep ? { keep: { [name]: keep } } : {};
2431
+ const plan = (root ? planProjectTidy(root, opts) : planGlobalTidy(opts)).plans.find((p) => p.name === name);
2432
+ if (!plan) return `${name}: nothing to tidy`;
2433
+ const r = applyTidy(plan, { git: allowGit ? "go" : "keep" });
2434
+ const steps = (list) => list.map((s) => describeStep(s, root)).join(", ");
2435
+ if (!r.held.length) return r.applied.length ? `${name}: ${steps(r.applied)}` : `${name}: nothing changed`;
2436
+ return {
2437
+ message: `${name}: ${r.applied.length ? `${steps(r.applied)}; ` : ""}held back ${steps(r.held)}, because git tracks it`,
2438
+ then: {
2439
+ label: "Also change what git tracks",
2440
+ preview: `${steps(r.held)}: these changes will show in git status in ${repo}. Replaced copies go to Settings \u203A Backups.`,
2441
+ run: (w) => w.ops.tidy(repo, name, { keep, allowGit: true })
2442
+ }
2443
+ };
2444
+ },
2068
2445
  unloadGlobal: (m) => {
2069
2446
  if (!m.broken && !latestVersion(m.name)) importSkill(m.path);
2070
2447
  const r = unloadGlobal(m.path, m.links);
2071
- 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}`);
2072
2449
  },
2073
2450
  deleteGlobal: (m) => {
2074
2451
  const r = deleteGlobal(m.path, m.links);
2075
- 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}`);
2076
2453
  },
2077
2454
  keepGlobal: (name, keep) => (setKeepGlobal([name], keep), keep ? `${name} marked as global on purpose` : `${name} is no longer marked as global on purpose`),
2078
- moveGlobal: (m, repos, group) => {
2079
- 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)`;
2080
- if (group) recordOrigin(m.name, `group: ${group}`);
2081
- const results = repos.map((r2) => [r2, addSkill(rootOf(r2), m.name)]);
2082
- const skipped = results.filter(([, c]) => c.action === "skipped");
2083
- if (skipped.length)
2084
- return `${m.name}: not added to ${skipped.map(([r2, c]) => `${r2} (${c.reason})`).join(", ")}, so it still loads globally`;
2085
- const blocked = results.filter(([, c]) => c.blocked?.length).map(([r2]) => r2);
2086
- const r = unloadGlobal(m.path, m.links);
2087
- if (!r.ok) return `${m.name}: ${r.reason}`;
2088
- return `${m.name} now loads only in ${repos.join(", ")}${blocked.length ? ` (some agents can't see it in ${blocked.join(", ")}: git tracks the folder)` : ""}`;
2455
+ linkGlobal: (m, agents) => {
2456
+ const r = linkGlobal(m.path, agents);
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;
2461
+ },
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;
2089
2510
  },
2090
2511
  createSkill: (name) => {
2091
2512
  const r = createSkill(name, "");
2092
- 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}`);
2514
+ },
2515
+ pluginOffHere: (repo, id) => {
2516
+ const root = rootOf(repo);
2517
+ const plugin = claudePlugins(skillsLoadedIn(root, machineSkills()), root).find((p) => p.id === id);
2518
+ if (!plugin) return failed(`${id} isn't on in ${repo}`);
2519
+ const r = turnOffIn(plugin, root);
2520
+ return r.ok ? r.message : failed(r.message);
2093
2521
  },
2094
2522
  replacePlugin: (id, repos) => {
2095
2523
  const skills = machineSkills().filter((s) => s.kind === "plugin" && s.origin === id && !s.broken);
2096
2524
  const clashes = skills.filter((s) => importSkill(s.path).status === "exists").map((s) => s.name);
2097
- 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`);
2098
2526
  const skipped = repos.flatMap((r) => skills.flatMap((s) => {
2099
2527
  const c = addSkill(rootOf(r), s.name);
2100
2528
  return c.action === "skipped" ? [`${s.name} in ${r} (${c.reason})`] : [];
2101
2529
  }));
2102
- if (skipped.length) return `${id} kept: not added ${skipped.join(", ")}`;
2530
+ if (skipped.length) return failed(`${id} kept: not added ${skipped.join(", ")}`);
2103
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(", ")}` : ""}`;
2104
2532
  const bin = claudeBinary();
2105
2533
  const claude = (...args) => spawnSync3(bin, ["plugin", ...args], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: 2e4 });
@@ -2115,7 +2543,7 @@ function realOps(roots, backups) {
2115
2543
  try {
2116
2544
  setPluginEnabled(id, false);
2117
2545
  } catch (e) {
2118
- 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`);
2119
2547
  }
2120
2548
  return `${copied}; turned ${id} off in ~/.claude/settings.json (${why})${synced ? "" : `: finish with /plugin uninstall ${id}`}`;
2121
2549
  },
@@ -2128,13 +2556,13 @@ function realOps(roots, backups) {
2128
2556
  from.push([...roots].find(([, r2]) => r2 === root)?.[0] ?? basename(root));
2129
2557
  }
2130
2558
  const r = deleteLibrarySkill(name);
2131
- if (!r.ok) return `${name}: ${r.reason}`;
2559
+ if (!r.ok) return failed(`${name}: ${r.reason}`);
2132
2560
  return `Deleted ${name} from your library${from.length ? ` and from ${from.join(", ")}` : ""} (in Settings \u203A Backups)`;
2133
2561
  },
2134
2562
  restoreBackup: (i) => {
2135
2563
  const b = backups[i];
2136
2564
  const r = b.kind === "plugin" ? restorePlugin(b) : restoreBackup(b);
2137
- 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}`);
2138
2566
  },
2139
2567
  setAgents: (ids) => (setHarnesses(ids), "Saved your agents"),
2140
2568
  installVersion: (repo, name, version) => withLinks(addSkill(rootOf(repo), name, { version, force: true }), repo, name),
@@ -2143,20 +2571,28 @@ function realOps(roots, backups) {
2143
2571
  addRoot: (path) => {
2144
2572
  const root = addRoot(path);
2145
2573
  discoverProjects();
2146
- return `Scanning ${tildify3(root)} for repos`;
2574
+ return `Scanning ${tildify4(root)} for repos`;
2147
2575
  },
2148
- 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`),
2149
2577
  rescan: () => (discoverProjects(), "Looked for repos in your project folders again"),
2150
2578
  hide: (repo) => (setHidden(rootOf(repo), true), `Hid ${repo}: Settings \u203A Hidden repos brings it back`),
2151
- 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`),
2152
2580
  openFolder: (path) => {
2153
2581
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
2154
2582
  spawn(cmd, [path], { detached: true, stdio: "ignore" }).unref();
2155
- return `Opened ${tildify3(path)}`;
2583
+ return `Opened ${tildify4(path)}`;
2156
2584
  },
2157
2585
  copy: (text, what) => {
2158
2586
  const r = copyText(text);
2159
- 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");
2160
2596
  },
2161
2597
  repoInfo: (repo) => {
2162
2598
  if (!info.has(repo)) {