skilllib 1.3.1 → 1.6.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.
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  add,
4
+ agentSkill,
4
5
  doctor,
5
6
  error,
6
7
  folders,
@@ -13,9 +14,11 @@ import {
13
14
  outdated,
14
15
  parseArgs,
15
16
  projects,
17
+ refreshAgentSkill,
16
18
  remove,
17
19
  restore,
18
20
  scan,
21
+ setJsonMode,
19
22
  show,
20
23
  status,
21
24
  sync,
@@ -23,7 +26,7 @@ import {
23
26
  unhide,
24
27
  update,
25
28
  usage
26
- } from "./chunk-TWQQHXVG.js";
29
+ } from "./chunk-PHRUPZGX.js";
27
30
 
28
31
  // src/index.ts
29
32
  import { readFileSync } from "fs";
@@ -64,6 +67,13 @@ Health:
64
67
  skilllib restore [name] Restore something skilllib moved out of the way
65
68
  skilllib unhide <path> Show a hidden project again
66
69
 
70
+ Agents:
71
+ skilllib agent-skill [install|remove]
72
+ Teach your agents to use skilllib, so you can ask them
73
+ "which skills can you use here?" in any repo
74
+ --json Machine-readable output (status list show projects outdated
75
+ usage doctor add remove sync update import agent-skill)
76
+
67
77
  Options: --force (overwrite local edits) --allow-tracked (link into git-tracked skill folders) --days N
68
78
  `.trim();
69
79
  function packageVersion() {
@@ -77,11 +87,14 @@ function packageVersion() {
77
87
  async function main() {
78
88
  const [command, ...rest] = process.argv.slice(2);
79
89
  const args = parseArgs(rest);
90
+ setJsonMode(args.json);
91
+ if (!["--version", "-v", "version", "help", "--help", "-h"].includes(command ?? "")) refreshAgentSkill();
80
92
  switch (command) {
81
93
  case void 0:
82
- return process.stdin.isTTY && process.stdout.isTTY ? (await import("./tui-H5IQAGAL.js")).tui() : status(args);
94
+ return process.stdin.isTTY && process.stdout.isTTY ? (await import("./tui-WYVSDGHD.js")).tui() : status(args);
83
95
  case "ui":
84
- return (await import("./tui-H5IQAGAL.js")).tui();
96
+ if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("`skilllib ui` needs an interactive terminal. Agents and scripts: use `skilllib status --json`.");
97
+ return (await import("./tui-WYVSDGHD.js")).tui();
85
98
  case "status":
86
99
  return status(args);
87
100
  case "add":
@@ -107,7 +120,7 @@ async function main() {
107
120
  case "scan":
108
121
  return scan(args);
109
122
  case "projects":
110
- return projects();
123
+ return projects(args);
111
124
  case "link":
112
125
  return link(args);
113
126
  case "tidy":
@@ -132,6 +145,8 @@ async function main() {
132
145
  return doctor(args);
133
146
  case "usage":
134
147
  return usage(args);
148
+ case "agent-skill":
149
+ return agentSkill(args);
135
150
  case "--version":
136
151
  case "-v":
137
152
  case "version":
@@ -3,15 +3,21 @@ import {
3
3
  addRoot,
4
4
  addSkill,
5
5
  allowTrackedLinks,
6
+ applyTidy,
6
7
  claudeBinary,
8
+ claudePlugins,
9
+ copyLabel,
7
10
  createSkill,
11
+ cursorPluginSkills,
8
12
  deleteGlobal,
9
13
  deleteLibrarySkill,
14
+ describeStep,
10
15
  discoverProjects,
11
16
  enabledHarnesses,
12
17
  findProjectRoot,
13
18
  forgetLatest,
14
19
  gitInfo,
20
+ gitVisibleSteps,
15
21
  harness,
16
22
  harnessesChosen,
17
23
  importSkill,
@@ -22,8 +28,11 @@ import {
22
28
  librarySkillDir,
23
29
  librarySkills,
24
30
  linkEverywhere,
31
+ linkGlobal,
25
32
  listBackups,
26
33
  machineSkills,
34
+ planGlobalTidy,
35
+ planProjectTidy,
27
36
  pluginBackups,
28
37
  projectOfFactory,
29
38
  projectStatus,
@@ -45,13 +54,15 @@ import {
45
54
  setKeepGlobal,
46
55
  setPluginEnabled,
47
56
  skilllibHome,
57
+ skillsLoadedIn,
48
58
  syncProject,
59
+ turnOffIn,
49
60
  unloadGlobal,
50
61
  updateProject,
51
62
  userHome,
52
63
  versionHistory,
53
64
  visibleProjects
54
- } from "./chunk-TWQQHXVG.js";
65
+ } from "./chunk-PHRUPZGX.js";
55
66
 
56
67
  // src/tui/index.tsx
57
68
  import { render } from "ink";
@@ -299,17 +310,71 @@ function dailyUses(w, name, onlyRepo) {
299
310
  }
300
311
  return days;
301
312
  }
313
+ function machineIn(w, p) {
314
+ return p.machine ?? w.machine;
315
+ }
302
316
  function usable(w, projectName) {
303
317
  const p = project(w, projectName);
304
318
  const uses = (n) => w.usage[projectName]?.[n] ?? 0;
305
319
  return [
306
320
  ...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 }))
321
+ ...machineIn(w, p).map((m) => ({ name: m.name, source: m.source, where: m.where, agents: m.agents, uses: uses(m.name), machine: m }))
308
322
  ];
309
323
  }
310
324
  function lib(w, name) {
311
325
  return w.library.find((l) => l.name === name);
312
326
  }
327
+ var agentNames = (ids) => ids.map((a) => harness(a).name).join(", ");
328
+ function dupeIssues(repo, name, d) {
329
+ const g = repo === null ? "-g" : "";
330
+ if (d.differ)
331
+ return [
332
+ {
333
+ id: `conflict${g}:${name}`,
334
+ severity: "problem",
335
+ title: `Copies differ: ${d.differ.map((c) => c.label + (c.runs.length ? ` (${agentNames(c.runs)})` : "")).join(" vs ")}`,
336
+ short: "Copies differ",
337
+ decision: true,
338
+ fixes: d.differ.filter((c) => c.canWin).map((c) => ({
339
+ label: `Keep the ${c.label} copy`,
340
+ 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." : ""}`,
341
+ run: (w) => w.ops.tidy(repo, name, { keep: c.dir })
342
+ }))
343
+ }
344
+ ];
345
+ return [
346
+ {
347
+ id: `copies${g}:${name}`,
348
+ severity: "warning",
349
+ title: repo ? "More copies or links in this repo than your agents need" : "Identical copies in several global folders",
350
+ short: "Extra copies",
351
+ decision: false,
352
+ fixes: [
353
+ {
354
+ label: "Keep one copy, plus the links your agents need",
355
+ 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"}.` : ""}`,
356
+ run: (w) => w.ops.tidy(repo, name)
357
+ }
358
+ ]
359
+ }
360
+ ];
361
+ }
362
+ function cursorPluginIssue(name, plugin) {
363
+ return {
364
+ id: `cursor-plugin:${name}`,
365
+ severity: "warning",
366
+ title: `Also in the Cursor plugin ${plugin}: Cursor lists both while it's on`,
367
+ short: "Also in a Cursor plugin",
368
+ decision: true,
369
+ fixes: [
370
+ {
371
+ label: "Turn the plugin off in Cursor (Settings \u203A Plugins)",
372
+ preview: "skilllib can't read or change Cursor's plugins. Your skill wins once the plugin is off.",
373
+ run: () => `Turn ${plugin} off in Cursor; skilllib can't tell whether it's on`
374
+ }
375
+ ]
376
+ };
377
+ }
313
378
  function globalFixes(m) {
314
379
  return [
315
380
  {
@@ -355,26 +420,38 @@ function machineIssues(w, m) {
355
420
  }
356
421
  ]
357
422
  });
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)
423
+ if (m.source === "global" && m.dupes) issues.push(...dupeIssues(null, m.name, m.dupes));
424
+ if (m.source === "global" && m.cursorPlugin && m.agents.includes("cursor")) issues.push(cursorPluginIssue(m.name, m.cursorPlugin));
425
+ const seen = new Set(w.machine.filter((x) => x.name === m.name && !x.broken).flatMap((x) => x.agents));
426
+ const blind = m.source === "global" && m.agents.length && !m.dupes ? w.agents.filter((a) => !seen.has(a)) : [];
427
+ const names = blind.map((a) => harness(a).name).join(", ");
428
+ const link = {
429
+ label: `Link it for ${names}`,
430
+ 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.`,
431
+ run: (w2) => w2.ops.linkGlobal(m, blind)
432
+ };
433
+ if (m.kept && blind.length)
434
+ 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] });
435
+ if (m.source === "global" && !m.kept) {
436
+ const [move, ...rest] = globalFixes(m);
360
437
  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",
438
+ id: `global:${m.name}`,
439
+ severity: "warning",
440
+ title: blind.length ? `Global, not reviewed: loads in every repo, but ${names} can't see it` : "Global, not reviewed: loads in every repo",
441
+ short: blind.length ? `Not reviewed \xB7 ${names} can't see it` : "Not reviewed",
365
442
  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) }))
443
+ fixes: blind.length ? [move, link, ...rest] : [move, ...rest]
367
444
  });
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) });
445
+ }
370
446
  return issues;
371
447
  }
372
448
  function issuesOf(w, projectName, u) {
373
449
  const s = u.local;
374
450
  const issues = [];
375
451
  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));
452
+ const loaded = machineIn(w, project(w, projectName));
453
+ const g = loaded.find((m) => m.source === "global" && shares(m));
454
+ const vendor = loaded.find((m) => m.source !== "global" && shares(m));
378
455
  const l = lib(w, s.name);
379
456
  const remove = {
380
457
  label: "Remove this repo's copy",
@@ -399,7 +476,23 @@ function issuesOf(w, projectName, u) {
399
476
  decision: false,
400
477
  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
478
  });
402
- if (g)
479
+ if (g?.kept)
480
+ issues.push({
481
+ id: `kept-g:${s.name}`,
482
+ severity: "warning",
483
+ title: `Also global in ${g.where}, and you keep it global: this repo's copy is extra`,
484
+ short: "Extra: you keep it global",
485
+ decision: true,
486
+ fixes: [
487
+ ...s.source !== "repo" ? [{ ...remove, label: "Remove this repo's copy, keep it global" }] : [],
488
+ {
489
+ 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.`,
491
+ run: (w2) => w2.ops.unloadGlobal(g)
492
+ }
493
+ ]
494
+ });
495
+ else if (g)
403
496
  issues.push({
404
497
  id: `twice-g:${s.name}`,
405
498
  severity: "problem",
@@ -424,13 +517,19 @@ function issuesOf(w, projectName, u) {
424
517
  decision: true,
425
518
  fixes: [
426
519
  ...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.",
520
+ vendor.source === "plugin" ? {
521
+ 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.`,
523
+ run: (w2) => w2.ops.pluginOffHere(projectName, vendor.where)
524
+ } : {
525
+ label: `Turn it off in ${vendor.where}`,
526
+ preview: "skilllib can't turn it off for you.",
430
527
  run: () => `Turn ${vendor.where} off at its source; skilllib picks it up next time`
431
528
  }
432
529
  ]
433
530
  });
531
+ if (s.dupes) issues.push(...dupeIssues(projectName, s.name, s.dupes));
532
+ if (s.cursorPlugin && s.agents.includes("cursor")) issues.push(cursorPluginIssue(s.name, s.cursorPlugin));
434
533
  if (s.source === "lib" && s.edited)
435
534
  issues.push({
436
535
  id: `edited:${s.name}`,
@@ -468,7 +567,8 @@ function issuesOf(w, projectName, u) {
468
567
  }
469
568
  ]
470
569
  });
471
- if (s.source !== "lib" && s.library === "differs")
570
+ const differ = !!s.dupes?.differ;
571
+ if (s.source !== "lib" && s.library === "differs" && !differ)
472
572
  issues.push({
473
573
  id: `differs:${s.name}`,
474
574
  severity: "hint",
@@ -480,7 +580,7 @@ function issuesOf(w, projectName, u) {
480
580
  ...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
581
  ]
482
582
  });
483
- if (s.source === "untracked" && s.library === "same")
583
+ if (s.source === "untracked" && s.library === "same" && !differ)
484
584
  issues.push({
485
585
  id: `adopt:${s.name}`,
486
586
  severity: "hint",
@@ -489,7 +589,7 @@ function issuesOf(w, projectName, u) {
489
589
  decision: false,
490
590
  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
591
  });
492
- if (s.source === "untracked" && !l && !vendor)
592
+ if (s.source === "untracked" && !l && !vendor && !differ)
493
593
  issues.push({
494
594
  id: `local:${s.name}`,
495
595
  severity: "hint",
@@ -589,6 +689,7 @@ function groupCandidates(w, u) {
589
689
  if (m?.source === "plugin") return [{ key: `plugin:${m.where}`, label: `\u29C9 ${m.where.split("@")[0]} plugin` }];
590
690
  if (m?.source === "claude.ai") return [{ key: "claude.ai", label: "claude.ai skills" }];
591
691
  if (m?.source === "cursor") return [{ key: "cursor", label: "Cursor built-ins" }];
692
+ if (m?.source === "system") return [{ key: `system:${m.where}`, label: m.where }];
592
693
  if (m) {
593
694
  const origin = originGroup(m.origin);
594
695
  return [...origin ? [origin] : [], ...byTime("global", m.installed), ...byWord("global", m.name)];
@@ -775,7 +876,7 @@ function TitleRow({ cells, width }) {
775
876
  }
776
877
  var fixOption = (fix) => ({ label: fix.label, fix });
777
878
  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 };
879
+ var COPY_RANK = { lib: 0, repo: 0, untracked: 0, global: 1, plugin: 2, "claude.ai": 3, cursor: 3, system: 3 };
779
880
  function byName(copies) {
780
881
  const groups = /* @__PURE__ */ new Map();
781
882
  for (const c of copies) groups.set(c.name, [...groups.get(c.name) ?? [], c]);
@@ -955,7 +1056,7 @@ function globalItems(w, nameW, ui) {
955
1056
  }
956
1057
  function globalActions(w, u, ui) {
957
1058
  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";
1059
+ const atSource = m.source === "plugin" ? "Turn it off with /plugin in Claude Code" : m.source === "claude.ai" ? "Turn it off in claude.ai \u203A Settings" : m.source === "system" ? `An admin manages ${m.where}` : "Cursor manages it";
959
1060
  return [
960
1061
  ...machineActions(m).map(fixOption),
961
1062
  ...m.source === "plugin" ? [fixOption(replacePluginFix(w, m.where))] : [],
@@ -1903,7 +2004,7 @@ function machineSkill(s, kept) {
1903
2004
  const global = s.kind === "global" || s.kind === "skills.sh";
1904
2005
  return {
1905
2006
  name: s.name,
1906
- source: global ? "global" : s.kind === "plugin" ? "plugin" : s.kind === "claude.ai" ? "claude.ai" : "cursor",
2007
+ source: global ? "global" : s.kind === "plugin" ? "plugin" : s.kind === "claude.ai" ? "claude.ai" : s.kind === "system" ? "system" : "cursor",
1907
2008
  where: global ? tildify3(dirname(s.path)) : s.kind === "claude.ai" ? "claude.ai account" : s.kind === "built-in" ? `${s.origin} built-in` : s.origin,
1908
2009
  ...s.kind === "skills.sh" ? { origin: `npx skills: ${s.origin}` } : {},
1909
2010
  path: s.path,
@@ -1915,6 +2016,13 @@ function machineSkill(s, kept) {
1915
2016
  ...s.kind === "plugin" ? { pluginParts: pluginParts(dirname(dirname(s.path))) } : {}
1916
2017
  };
1917
2018
  }
2019
+ function dupesOf(r, root, git2 = null) {
2020
+ const out = /* @__PURE__ */ new Map();
2021
+ 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
+ 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 })) });
2024
+ return out;
2025
+ }
1918
2026
  function loadWorld() {
1919
2027
  forgetLatest();
1920
2028
  const agents = enabledHarnesses();
@@ -1938,20 +2046,39 @@ function loadWorld() {
1938
2046
  return { name: s.name, latest: latestVersion(s.name)?.version ?? 1, origin: origins[s.name] };
1939
2047
  });
1940
2048
  const kept = keptGlobal();
1941
- const machine = machineSkills(agents).map((s) => {
2049
+ const cursorPlugin = new Map(agents.includes("cursor") ? cursorPluginSkills().map((s) => [s.name, s.plugin]) : []);
2050
+ const globalDupes = dupesOf(planGlobalTidy({ enabled: agents }), null);
2051
+ const sourced = machineSkills(agents);
2052
+ const machine = sourced.map((s) => {
1942
2053
  descriptions[s.name] ??= s.description;
1943
- return machineSkill(s, kept);
2054
+ const m = machineSkill(s, kept);
2055
+ if (m.source !== "global" || m.broken) return m;
2056
+ const dupes = globalDupes.get(m.name);
2057
+ const plugin = cursorPlugin.get(m.name);
2058
+ return { ...m, ...dupes ? { dupes } : {}, ...plugin ? { cursorPlugin: plugin } : {} };
1944
2059
  });
2060
+ const plugins = (root) => {
2061
+ const loaded = skillsLoadedIn(root, sourced, agents);
2062
+ if (loaded === sourced) return {};
2063
+ for (const s of loaded) descriptions[s.name] ??= s.description;
2064
+ return { machine: loaded.map((s) => machine[sourced.indexOf(s)] ?? machineSkill(s, kept)) };
2065
+ };
1945
2066
  const projects = roots.map((root) => {
1946
2067
  const status = projectStatus(root);
1947
2068
  const hasManifest = existsSync3(join2(root, "skilllib.json"));
1948
2069
  const git2 = status.length || hasManifest ? gitInfo(root) : null;
1949
2070
  for (const s of status) if (!descriptions[s.name] && !s.state.includes("missing")) descriptions[s.name] = readSkillInfo(s.path).description;
2071
+ const dupes = status.length ? dupesOf(planProjectTidy(root, { enabled: agents }), root, git2) : /* @__PURE__ */ new Map();
1950
2072
  return {
1951
2073
  name: names.get(root),
1952
2074
  path: root,
1953
2075
  manifest: hasManifest ? git2 ? git2.of("skilllib.json") : "no git" : "none",
1954
- skills: status.map((s) => localSkill(root, s, git2))
2076
+ skills: status.map((s) => {
2077
+ const d = dupes.get(s.name);
2078
+ const plugin = cursorPlugin.get(s.name);
2079
+ return { ...localSkill(root, s, git2), ...d ? { dupes: d } : {}, ...plugin ? { cursorPlugin: plugin } : {} };
2080
+ }),
2081
+ ...plugins(root)
1955
2082
  };
1956
2083
  });
1957
2084
  const backups = [...listBackups(), ...pluginBackups()].sort((a, b) => b.movedAt.localeCompare(a.movedAt));
@@ -2065,6 +2192,24 @@ function realOps(roots, backups) {
2065
2192
  return withLinks(addSkill(rootOf(repo), name), repo, name);
2066
2193
  },
2067
2194
  copyToLibrary: (repo, name) => toLibrary(find(repo, name).path),
2195
+ tidy: (repo, name, { keep, allowGit = false } = {}) => {
2196
+ const root = repo === null ? null : rootOf(repo);
2197
+ const opts = keep ? { keep: { [name]: keep } } : {};
2198
+ const plan = (root ? planProjectTidy(root, opts) : planGlobalTidy(opts)).plans.find((p) => p.name === name);
2199
+ if (!plan) return `${name}: nothing to tidy`;
2200
+ const r = applyTidy(plan, { git: allowGit ? "go" : "keep" });
2201
+ 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;
2204
+ return {
2205
+ message,
2206
+ then: {
2207
+ label: "Also change what git tracks",
2208
+ preview: `${steps(r.held)}: these changes will show in git status in ${repo}. Replaced copies go to Settings \u203A Backups.`,
2209
+ run: (w) => w.ops.tidy(repo, name, { keep, allowGit: true })
2210
+ }
2211
+ };
2212
+ },
2068
2213
  unloadGlobal: (m) => {
2069
2214
  if (!m.broken && !latestVersion(m.name)) importSkill(m.path);
2070
2215
  const r = unloadGlobal(m.path, m.links);
@@ -2075,6 +2220,12 @@ function realOps(roots, backups) {
2075
2220
  return r.ok ? m.broken ? `Removed the broken link ${m.name}` : `Deleted ${m.name} (in Settings \u203A Backups)` : `${m.name}: ${r.reason}`;
2076
2221
  },
2077
2222
  keepGlobal: (name, keep) => (setKeepGlobal([name], keep), keep ? `${name} marked as global on purpose` : `${name} is no longer marked as global on purpose`),
2223
+ linkGlobal: (m, agents) => {
2224
+ 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}`;
2228
+ },
2078
2229
  moveGlobal: (m, repos, group) => {
2079
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)`;
2080
2231
  if (group) recordOrigin(m.name, `group: ${group}`);
@@ -2091,6 +2242,14 @@ function realOps(roots, backups) {
2091
2242
  const r = createSkill(name, "");
2092
2243
  return r.ok ? `Created ${name} in your library: edit its SKILL.md from Your skills` : `${name}: ${r.reason}`;
2093
2244
  },
2245
+ pluginOffHere: (repo, id) => {
2246
+ const root = rootOf(repo);
2247
+ const plugin = claudePlugins(skillsLoadedIn(root, machineSkills()), root).find((p) => p.id === id);
2248
+ if (!plugin) return `${id} isn't on in ${repo}`;
2249
+ const r = turnOffIn(plugin, root);
2250
+ if (!r.ok) throw new Error(r.message);
2251
+ return r.message;
2252
+ },
2094
2253
  replacePlugin: (id, repos) => {
2095
2254
  const skills = machineSkills().filter((s) => s.kind === "plugin" && s.origin === id && !s.broken);
2096
2255
  const clashes = skills.filter((s) => importSkill(s.path).status === "exists").map((s) => s.name);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skilllib",
3
- "version": "1.3.1",
3
+ "version": "1.6.0",
4
4
  "description": "Manage your agent skills like dependencies. One library for all your skills; each repo gets only the ones it needs, and every agent can see them. Works with Claude Code, Cursor, Codex and Zed.",
5
5
  "keywords": [
6
6
  "agent-skills",
@@ -32,6 +32,7 @@
32
32
  },
33
33
  "files": [
34
34
  "dist",
35
+ "skills",
35
36
  "README.md",
36
37
  "LICENSE"
37
38
  ],
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: skilllib
3
+ description: Agent skills (SKILL.md) loaded in this repo and the user's skill library, via the skilllib CLI. Use for which skills you can use here and where each comes from, which library skills to add, what's out of date, broken or duplicated, and adding, removing or updating skills.
4
+ allowed-tools: Bash(skilllib:*)
5
+ ---
6
+
7
+ # skilllib
8
+
9
+ The user manages agent skills with the `skilllib` CLI. Their **library** (`~/.skilllib/library`, "my skills", "the registry") holds versioned skills that load nowhere until added to a repo. A repo pins its skills in `skilllib.json`.
10
+
11
+ Always pass `--json` (one line of JSON on stdout; messages go to stderr). Never run bare `skilllib` or `skilllib ui`: that's an interactive app.
12
+
13
+ ## Reading
14
+
15
+ The **Now** section at the end has this repo's `status` and `list` output. Answer from it and don't run them again. If it shows the commands instead of their output, run both in parallel.
16
+
17
+ `status --json`: `skills` are this repo's own; `global` groups load in every repo; `issues` are everything skilllib finds wrong (out-of-date or missing skills, broken links, skills loaded twice), with either a `fix` (a safe repair; `skilllib doctor --fix` applies every fix, across all the user's projects) or `choices` (decisions the user makes in `skilllib` → Health). When asked what's out of date or broken, `issues` and each skill's `state` are the answer. Don't audit skill contents unless asked.
18
+
19
+ - `source`: `library` (added with skilllib) · `repo` (committed to `.agents/skills` by the team) · `local` (unmanaged folder) · `global` (`~/.claude/skills`, `~/.agents/skills` or `~/.codex/skills`) · `skills.sh` (`npx skills add`; `from` is the source repo) · `plugin` (Claude Code plugin; turn off with `/plugin`) · `claude.ai` (synced from the account) · `built-in` (bundled with an agent).
20
+ - Missing fields mean the usual: loaded by all of `agents`, state ok, in `.claude/skills`. `usesHere` counts Claude Code uses in this repo.
21
+
22
+ `list --json`: `inThisProject` (names) and `skills`, the library skills the repo doesn't have, each with a short `description` and the `projects` using it. To recommend, read the repo's README and manifests (see `files` in `status`) in one parallel step, then give a reason for each pick. Don't add a skill that already loads globally (it would load twice): name it, and say `skilllib` → Global → Clean up moves it into just the repos that need it. `show <name> --json` has the full SKILL.md.
23
+
24
+ Also: `usage --json` (Claude Code usage only, so "unused" is a hint), `projects --json` (every repo), `outdated --all --json`.
25
+
26
+ ## Changing (ask first unless the user asked for this exact change)
27
+
28
+ `add <name>...` · `remove <name>...` · `update [name...]` · `sync` (pinned versions) · `import <dir>` (into the library) · `doctor --fix`. Pass several names in one call. Each prints `[{name, action, from, to, dirs, reason, blocked}]`, which is the whole result: there's no need to check files afterwards.
29
+
30
+ - `skipped` + `reason`: nothing changed; tell the user. `--force` overwrites local edits, but only use it with their OK.
31
+ - `blocked`: git tracks that folder. Re-run with `--allow-tracked` only with their OK.
32
+ - Changes apply from the agent's next session. Remind the user to commit `skilllib.json` and `.claude/skills`.
33
+
34
+ To move global skills into repos, tell the user to run `skilllib` and open Global → Clean up.
35
+
36
+ ## Now
37
+
38
+ This repo (`skilllib status --json`):
39
+ !`skilllib status --json`
40
+
41
+ Library skills it could add (`skilllib list --json`):
42
+ !`skilllib list --json`