skilllib 1.0.2 → 1.1.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/README.md CHANGED
@@ -47,6 +47,7 @@ Most agents can't turn off a global skill for just one repo. Claude Code can, bu
47
47
  - **📦 Per-repo installs, pinned like dependencies.** Each repo lists its skills and versions in `skilllib.json`. `sync` reproduces them exactly, and `update` moves to the newest.
48
48
  - **🤝 One copy, every agent.** skilllib puts one real copy in the repo and links it into whichever folders your other agents read.
49
49
  - **🧹 A cleanup wizard for global skills.** For each global skill, pick the repos that should keep it, keep it global, or delete it. Usage numbers and descriptions help you decide.
50
+ - **✓ Global on purpose.** Some skills belong everywhere. Mark them once and skilllib stops warning about them.
50
51
  - **🔎 See everything in a repo:**
51
52
  - where each skill comes from (the repo, your library, global, a plugin, claude.ai…)
52
53
  - which agents load it
@@ -119,7 +120,8 @@ A repo has two lists:
119
120
  |---|---|---|
120
121
  | *From your skills* | Versioned skills you added from your library | Remove it from this repo |
121
122
  | *The repo's own* | Skills committed to the repo (e.g. your team's `.agents/skills`) | Copy it into your skills, so other repos can use it |
122
- | *⚠ Global · yours* | Your global skills: they load in every repo | Open the cleanup wizard |
123
+ | *⚠ Global · yours, not reviewed* | Your global skills you haven't decided about: they load in every repo | Open the cleanup wizard |
124
+ | *✓ Global · yours, on purpose* | Your global skills you chose to keep global | Open the cleanup wizard |
123
125
  | *Global · from vendors* | Plugins, claude.ai skills, Cursor built-ins | – (manage them at the source) |
124
126
 
125
127
  **Add skills** shows your skills that this repo doesn't have yet. `space` adds one.
@@ -171,13 +173,23 @@ Edit a skill once in **Your skills** (Enter → *Edit SKILL.md*). Every repo tha
171
173
  Open **Global → ⚠ Clean up…**, or press `space` on any of your global skills. For each skill, `space` cycles through:
172
174
 
173
175
  - **◉ move:** installed into the repos you tick on the right. Repos that already have it, or where Claude Code used it, are pre-ticked.
174
- - **○ keep global**
176
+ - **○ keep global**: skilllib remembers this choice, and the next cleanup skips the skill
175
177
  - **✕ delete** (`d`): removed without keeping a copy in your library
176
178
 
177
179
  Each skill shows how often Claude Code used it, and the panel below shows its description and where it was used. Review, apply, and your globals are gone: moved ones are in your library and in the right repos. Originals go to a backup you can restore from **Health**.
178
180
 
179
181
  You can also delete one skill (Enter → **Delete**) or a whole group (Enter → **Delete all N**) straight from Global.
180
182
 
183
+ ### Keeping a skill global on purpose
184
+
185
+ Some skills are useful in every repo, for example a commit-message skill. Mark them so skilllib stops asking about them:
186
+
187
+ - Enter on the skill (in Global or in a repo) → **✓ Keep global on purpose**. Enter on a group → **Keep all N global on purpose**.
188
+ - In the cleanup wizard, every skill you leave on **○ keep global** is marked when you apply.
189
+ - From the command line: `skilllib global keep <name>`.
190
+
191
+ A marked skill shows **✓ global** instead of **⚠ loaded globally**, and has its own group in a repo. The cleanup wizard hides it (`k` shows it again), and review prompts tell the agent that you chose to keep it global. The mark only records your decision: no files move, and agents load the skill as before. Duplicate warnings (⧉) still show. To undo, use Enter → **Unmark** or `skilllib global unkeep <name>`.
192
+
181
193
  ### Ask an agent to review your skills
182
194
 
183
195
  Not sure what to keep? **Copy review prompt** puts a ready-to-paste prompt on your clipboard (also saved to `~/.skilllib/review-prompt.md`). For each skill, it includes:
@@ -277,6 +289,7 @@ skilllib link [--all] make every skill here usable by all your agents
277
289
 
278
290
  skilllib list | show <name> your library
279
291
  skilllib import <dir>... add skill folders to your library (--global: all your global skills)
292
+ skilllib global [keep|unkeep] your global skills; mark the ones you keep global on purpose
280
293
  skilllib new <name> [desc] create a skill
281
294
 
282
295
  skilllib usage [--days N] which skills Claude Code used
@@ -294,7 +307,7 @@ Flags: `--force` (overwrite local edits), `--allow-tracked` (link into git-track
294
307
  | `<repo>/skilllib.json` | The repo's skills and pinned versions. **Commit it.** |
295
308
  | `~/.skilllib/library/` | Your skills (master copies). Worth putting under git. |
296
309
  | `~/.skilllib/store/` | Every version of every skill, immutable |
297
- | `~/.skilllib/config.json` | Your agents, project folders, hidden projects |
310
+ | `~/.skilllib/config.json` | Your agents, project folders, hidden projects, skills you keep global |
298
311
  | `~/.skilllib/trash/`, `global-backup/` | Everything skilllib removed, restorable from Health |
299
312
  | `~/.skilllib/review-prompt.md` | The last review prompt you copied |
300
313
 
@@ -328,7 +341,7 @@ npm link # use your checkout as `skilllib`
328
341
  1. Bump `version` in `package.json` and merge to `main`.
329
342
  2. Tag and push:
330
343
  ```bash
331
- git tag v1.0.2 && git push --tags
344
+ git tag v1.1.0 && git push --tags
332
345
  ```
333
346
 
334
347
  CI then tests on Linux, macOS and Windows, publishes to npm through [trusted publishing](https://docs.npmjs.com/trusted-publishers) (no token; provenance is automatic), and creates a GitHub release.
@@ -373,6 +373,19 @@ function allowTrackedLinks(root) {
373
373
  const config = readConfig();
374
374
  writeConfig({ ...config, agentsDirOk: [.../* @__PURE__ */ new Set([...config.agentsDirOk ?? [], root])].sort() });
375
375
  }
376
+ function keptGlobal() {
377
+ return new Set(readConfig().keepGlobal ?? []);
378
+ }
379
+ function setKeepGlobal(names, keep) {
380
+ const kept = keptGlobal();
381
+ const before = kept.size;
382
+ for (const n of names) {
383
+ if (keep) kept.add(n);
384
+ else kept.delete(n);
385
+ }
386
+ if (kept.size === before) return;
387
+ writeConfig({ ...readConfig(), keepGlobal: [...kept].sort() });
388
+ }
376
389
 
377
390
  // src/sources.ts
378
391
  import { existsSync as existsSync6, lstatSync, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync5, readlinkSync, realpathSync as realpathSync2, writeFileSync as writeFileSync4 } from "fs";
@@ -849,7 +862,9 @@ function unloadGlobal(path, links = [], { requireLibrary = true } = {}) {
849
862
  const broken = !existsSync7(join8(path, "SKILL.md"));
850
863
  if (requireLibrary && !broken && latestVersion(name) === null) return { ok: false, reason: "import it into the library first" };
851
864
  for (const link2 of links) if (isLink(link2)) stash(link2, "global-backup");
852
- return { ok: true, movedTo: stash(path, "global-backup") };
865
+ const movedTo = stash(path, "global-backup");
866
+ if (!globalSkillDirs().some((d) => entryExists(join8(d, name)))) setKeepGlobal([name], false);
867
+ return { ok: true, movedTo };
853
868
  }
854
869
  function createSkill(name, description) {
855
870
  if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) return { ok: false, reason: "use lowercase letters, digits and dashes" };
@@ -1498,6 +1513,28 @@ function link(args) {
1498
1513
  if (res.blocked.length) warn(`${label}git tracks ${res.blocked.join(", ")}; re-run with --allow-tracked to link there too`);
1499
1514
  }
1500
1515
  }
1516
+ function globalCommand(args) {
1517
+ const [action, ...names] = args.positional;
1518
+ const yours = machineSkills().filter((m) => m.movable && !m.broken);
1519
+ if (action === "keep" || action === "unkeep") {
1520
+ const unknown = names.filter((n) => !yours.some((m) => m.name === n));
1521
+ if (!names.length || unknown.length) {
1522
+ error(names.length ? `Not one of your global skills: ${unknown.join(", ")}` : `Usage: skilllib global ${action} <name>...`);
1523
+ process.exit(1);
1524
+ }
1525
+ setKeepGlobal(names, action === "keep");
1526
+ success(action === "keep" ? `Keeping ${names.join(", ")} global on purpose` : `${names.join(", ")} will show up in cleanup again`);
1527
+ } else if (action) {
1528
+ error("Usage: skilllib global [keep|unkeep <name>...]");
1529
+ process.exit(1);
1530
+ }
1531
+ const kept = keptGlobal();
1532
+ if (!yours.length) return info("None of your skills load globally.");
1533
+ for (const m of yours) info(`${kept.has(m.name) ? green("\u2713") : yellow("\u26A0")} ${m.name.padEnd(32)} ${dim(tildify(m.path))}`);
1534
+ const unreviewed = yours.filter((m) => !kept.has(m.name)).length;
1535
+ if (unreviewed) info(dim(`
1536
+ ${unreviewed} not reviewed. Keep one global on purpose: skilllib global keep <name>`));
1537
+ }
1501
1538
 
1502
1539
  export {
1503
1540
  userHome,
@@ -1522,6 +1559,8 @@ export {
1522
1559
  harnessesChosen,
1523
1560
  setHarnesses,
1524
1561
  allowTrackedLinks,
1562
+ keptGlobal,
1563
+ setKeepGlobal,
1525
1564
  machineSkills,
1526
1565
  libraryOrigins,
1527
1566
  librarySkills,
@@ -1567,5 +1606,6 @@ export {
1567
1606
  usage,
1568
1607
  show,
1569
1608
  harnessesCommand,
1570
- link
1609
+ link,
1610
+ globalCommand
1571
1611
  };
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  doctor,
5
5
  error,
6
6
  folders,
7
+ globalCommand,
7
8
  harnessesCommand,
8
9
  importSkills,
9
10
  link,
@@ -21,7 +22,7 @@ import {
21
22
  unhide,
22
23
  update,
23
24
  usage
24
- } from "./chunk-67BDL5SN.js";
25
+ } from "./chunk-HLCJQVHB.js";
25
26
 
26
27
  // src/index.ts
27
28
  import { readFileSync } from "fs";
@@ -36,6 +37,7 @@ Set up:
36
37
  skilllib harnesses [id...] Show or set the agents you use (claude-code cursor codex zed)
37
38
  skilllib import <dir>... Add skill folders to the library (--force replaces)
38
39
  skilllib import --global Copy ~/.claude/skills into the library
40
+ skilllib global [keep|unkeep <name>...] Your global skills; mark ones you keep global on purpose
39
41
 
40
42
  In a project:
41
43
  skilllib status This project's skills and their status
@@ -73,9 +75,9 @@ async function main() {
73
75
  const args = parseArgs(rest);
74
76
  switch (command) {
75
77
  case void 0:
76
- return process.stdin.isTTY && process.stdout.isTTY ? (await import("./tui-CQCVL6IK.js")).tui() : status(args);
78
+ return process.stdin.isTTY && process.stdout.isTTY ? (await import("./tui-YF7BQ7CD.js")).tui() : status(args);
77
79
  case "ui":
78
- return (await import("./tui-CQCVL6IK.js")).tui();
80
+ return (await import("./tui-YF7BQ7CD.js")).tui();
79
81
  case "status":
80
82
  return status(args);
81
83
  case "add":
@@ -115,6 +117,8 @@ async function main() {
115
117
  case "new":
116
118
  case "create":
117
119
  return newSkill(args);
120
+ case "global":
121
+ return globalCommand(args);
118
122
  case "restore":
119
123
  return restore(args);
120
124
  case "doctor":
@@ -17,6 +17,7 @@ import {
17
17
  importSkill,
18
18
  installDirs,
19
19
  isProjectCandidate,
20
+ keptGlobal,
20
21
  latestVersion,
21
22
  libraryOrigins,
22
23
  librarySkillDir,
@@ -36,6 +37,7 @@ import {
36
37
  scanUsage,
37
38
  setHarnesses,
38
39
  setHidden,
40
+ setKeepGlobal,
39
41
  skilllibHome,
40
42
  summarize,
41
43
  syncProject,
@@ -46,7 +48,7 @@ import {
46
48
  versionHistory,
47
49
  visibilityOf,
48
50
  visibleProjects
49
- } from "./chunk-67BDL5SN.js";
51
+ } from "./chunk-HLCJQVHB.js";
50
52
 
51
53
  // src/tui/index.tsx
52
54
  import { render } from "ink";
@@ -263,7 +265,8 @@ function loadSnapshot() {
263
265
  backups: listBackups(),
264
266
  roots: readConfig().roots,
265
267
  harnesses: enabledHarnesses(),
266
- harnessesChosen: harnessesChosen()
268
+ harnessesChosen: harnessesChosen(),
269
+ keptGlobal: keptGlobal()
267
270
  };
268
271
  }
269
272
  function projectRows(root, library, status = projectStatus(root)) {
@@ -351,6 +354,7 @@ var NEXT_MODE = { move: "keep", keep: "delete", delete: "move" };
351
354
  var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
352
355
  function CleanupWizard({
353
356
  skills,
357
+ kept,
354
358
  projects,
355
359
  usesIn,
356
360
  usesTotal,
@@ -367,11 +371,19 @@ function CleanupWizard({
367
371
  const [step, setStep] = useState("choose");
368
372
  const [focus, setFocus] = useState("skills");
369
373
  const [modes, setModes] = useState(
370
- () => new Map(skills.map((s) => [s.path, !initialSkill || s.name === initialSkill ? "move" : "keep"]))
374
+ () => new Map(skills.map((s) => [s.path, s.name === initialSkill || !initialSkill && !kept.has(s.name) ? "move" : "keep"]))
371
375
  );
372
376
  const modeOf = (s) => modes.get(s.path) ?? "keep";
373
- const setMode = (s, mode) => setModes((prev) => new Map(prev).set(s.path, mode));
374
- const [skillIndex, setSkillIndex] = useState(() => Math.max(0, skills.findIndex((s) => s.name === initialSkill)));
377
+ const [touched, setTouched] = useState(() => new Set(initialSkill ? [] : skills.map((s) => s.path)));
378
+ const setMode = (s, mode) => {
379
+ setModes((prev) => new Map(prev).set(s.path, mode));
380
+ setTouched((prev) => new Set(prev).add(s.path));
381
+ };
382
+ const [showKept, setShowKept] = useState(false);
383
+ const shown = (on) => skills.filter((s) => on || !kept.has(s.name) || s.name === initialSkill);
384
+ const list = shown(showKept);
385
+ const hiddenKept = skills.length - shown(false).length;
386
+ const [skillIndex, setSkillIndex] = useState(() => Math.max(0, list.findIndex((s) => s.name === initialSkill)));
375
387
  const [projectIndex, setProjectIndex] = useState(0);
376
388
  const [status, setStatus] = useState("");
377
389
  const [targets, setTargets] = useState(
@@ -379,9 +391,10 @@ function CleanupWizard({
379
391
  skills.map((s) => [s.path, new Set(projects.filter((p) => installedIn(s.name).includes(p) || (usesIn(s.name, p) ?? 0) > 0))])
380
392
  )
381
393
  );
382
- const skill = skills[skillIndex];
394
+ const skill = list[skillIndex];
383
395
  const chosen = skills.filter((s) => modeOf(s) === "move");
384
396
  const toDelete = skills.filter((s) => modeOf(s) === "delete");
397
+ const toKeep = skills.filter((s) => modeOf(s) === "keep" && touched.has(s.path) && !kept.has(s.name));
385
398
  const totalInstalls = chosen.reduce((n, s) => n + (targets.get(s.path)?.size ?? 0), 0);
386
399
  const orphans = chosen.filter((s) => (targets.get(s.path)?.size ?? 0) === 0);
387
400
  const toggleTarget = (s, project) => setTargets((prev) => {
@@ -417,7 +430,9 @@ function CleanupWizard({
417
430
  if (r.ok) unloaded++;
418
431
  else problems.push(`${s.name}: ${r.reason}`);
419
432
  }
420
- onDone({ unloaded, installs, deleted, problems });
433
+ setKeepGlobal(toKeep.map((s) => s.name), true);
434
+ setKeepGlobal([...chosen, ...toDelete].map((s) => s.name).filter((n) => kept.has(n)), false);
435
+ onDone({ unloaded, installs, deleted, kept: toKeep.length, problems });
421
436
  };
422
437
  inputRef.current = (input, key) => {
423
438
  if (key.ctrl && input === "c") return onCancel();
@@ -427,14 +442,19 @@ function CleanupWizard({
427
442
  return;
428
443
  }
429
444
  if (key.escape) return onCancel();
430
- if (input === "p") return setStatus(onReviewPrompt(skills));
431
- if (key.return) return chosen.length || toDelete.length ? setStep("review") : void 0;
445
+ if (input === "p") return setStatus(onReviewPrompt(list));
446
+ if (input === "k" && hiddenKept) {
447
+ const next = shown(!showKept);
448
+ setShowKept(!showKept);
449
+ return setSkillIndex(Math.max(0, next.findIndex((s) => s.path === skill?.path)));
450
+ }
451
+ if (key.return) return chosen.length || toDelete.length || toKeep.length ? setStep("review") : void 0;
432
452
  if (key.tab || key.rightArrow || key.leftArrow) {
433
453
  return setFocus(key.leftArrow ? "skills" : key.rightArrow ? "projects" : focus === "skills" ? "projects" : "skills");
434
454
  }
435
455
  if (focus === "skills") {
436
456
  if (key.upArrow) return setSkillIndex((i) => Math.max(0, i - 1));
437
- if (key.downArrow) return setSkillIndex((i) => Math.min(skills.length - 1, i + 1));
457
+ if (key.downArrow) return setSkillIndex((i) => Math.min(list.length - 1, i + 1));
438
458
  if (input === " " && skill) return setMode(skill, NEXT_MODE[modeOf(skill)]);
439
459
  if (input === "d" && skill) return setMode(skill, modeOf(skill) === "delete" ? "keep" : "delete");
440
460
  } else {
@@ -460,8 +480,9 @@ function CleanupWizard({
460
480
  /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", borderStyle: "round", borderColor: color.accent, paddingX: 2, paddingY: 1, height: bodyHeight + 2, children: [
461
481
  /* @__PURE__ */ jsx2(Text2, { color: color.text, children: [
462
482
  chosen.length ? `${plural(chosen.length, "skill")} ${chosen.length === 1 ? "moves" : "move"} into your projects (${plural(totalInstalls, "install")})` : "",
463
- toDelete.length ? `${plural(toDelete.length, "skill")} ${toDelete.length === 1 ? "is" : "are"} deleted` : ""
464
- ].filter(Boolean).join(" \xB7 ") + ". None of them will load in every repo anymore." }),
483
+ toDelete.length ? `${plural(toDelete.length, "skill")} ${toDelete.length === 1 ? "is" : "are"} deleted` : "",
484
+ toKeep.length ? `${plural(toKeep.length, "skill")} ${toKeep.length === 1 ? "stays" : "stay"} global on purpose` : ""
485
+ ].filter(Boolean).join(" \xB7 ") + "." }),
465
486
  /* @__PURE__ */ jsx2(Text2, { color: color.faint, children: "Moved skills are copied into Your skills first. Every original goes to a backup you can restore from Health." }),
466
487
  /* @__PURE__ */ jsx2(Text2, { children: " " }),
467
488
  lines.slice(0, Math.max(1, bodyHeight - 7)).map(({ s, where }) => /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate-end", children: [
@@ -471,6 +492,7 @@ function CleanupWizard({
471
492
  ] }, s.path)),
472
493
  lines.length > bodyHeight - 7 ? /* @__PURE__ */ jsx2(Text2, { color: color.faint, children: `\u2026 and ${lines.length - (bodyHeight - 7)} more` }) : null,
473
494
  toDelete.length ? /* @__PURE__ */ jsx2(Text2, { color: color.red, wrap: "truncate-end", children: `\u2715 delete (not kept in Your skills): ${toDelete.map((s) => s.name).join(", ")}` }) : null,
495
+ toKeep.length ? /* @__PURE__ */ jsx2(Text2, { color: color.muted, wrap: "truncate-end", children: `\u2713 keep global, and skip next cleanup: ${toKeep.map((s) => s.name).join(", ")}` }) : null,
474
496
  /* @__PURE__ */ jsx2(Box2, { flexGrow: 1 }),
475
497
  orphans.length ? /* @__PURE__ */ jsx2(Text2, { color: color.yellow, children: `${plural(orphans.length, "skill")} won't be in any project \u2014 add them later from "Add skills".` }) : null
476
498
  ] }),
@@ -490,7 +512,7 @@ function CleanupWizard({
490
512
  const detailHeight = height >= 26 ? 7 : height >= 20 ? 5 : 0;
491
513
  const listHeight = bodyHeight - detailHeight;
492
514
  const usesText = (n) => n === null ? "\u2026" : n === 0 ? "unused" : `${n} use${n === 1 ? "" : "s"}`;
493
- const skillRows = skills.map((s) => {
515
+ const skillRows = list.map((s) => {
494
516
  const mode = modeOf(s);
495
517
  const n = targets.get(s.path)?.size ?? 0;
496
518
  return {
@@ -500,7 +522,7 @@ function CleanupWizard({
500
522
  { text: s.name, grow: true, color: mode === "keep" ? color.muted : mode === "delete" ? color.red : void 0 },
501
523
  { text: usesText(usesTotal(s.name)).padStart(9), width: 9, color: usesTotal(s.name) ? color.text : color.faint },
502
524
  {
503
- text: mode === "keep" ? " keep global" : mode === "delete" ? " delete" : n ? ` \u2192 ${plural(n, "project")}` : " \u2192 no project",
525
+ text: mode === "keep" ? kept.has(s.name) ? " \u2713 kept global" : " keep global" : mode === "delete" ? " delete" : n ? ` \u2192 ${plural(n, "project")}` : " \u2192 no project",
504
526
  width: 15,
505
527
  color: mode === "keep" ? color.faint : mode === "delete" ? color.red : n ? color.green : color.yellow
506
528
  }
@@ -526,19 +548,19 @@ function CleanupWizard({
526
548
  ] }),
527
549
  /* @__PURE__ */ jsxs2(Box2, { height: 2, paddingX: 1, flexDirection: "column", children: [
528
550
  /* @__PURE__ */ jsx2(Text2, { color: color.text, wrap: "truncate-end", children: "These load in every repo, so every agent reads them all the time. Pick the projects that should keep each one." }),
529
- /* @__PURE__ */ jsx2(Text2, { color: color.faint, wrap: "truncate-end", children: status ? `\u2713 ${status}` : "\u25C9 move into the ticked projects \xB7 \u25CB keep global \xB7 \u2715 delete. Pre-ticked: projects that have it or used it. p: ask an agent." })
551
+ /* @__PURE__ */ jsx2(Text2, { color: color.faint, wrap: "truncate-end", children: status ? `\u2713 ${status}` : "\u25C9 move into the ticked projects \xB7 \u25CB keep global (remembered) \xB7 \u2715 delete. Pre-ticked: projects that have it or used it. p: ask an agent." })
530
552
  ] }),
531
553
  /* @__PURE__ */ jsxs2(Box2, { height: listHeight, children: [
532
554
  /* @__PURE__ */ jsx2(
533
555
  ListPanel,
534
556
  {
535
- title: `Global skills \xB7 ${chosen.length} move \xB7 ${toDelete.length} delete \xB7 uses ${usageDays}d`,
557
+ title: `Global skills \xB7 ${chosen.length} move \xB7 ${toDelete.length} delete${hiddenKept && !showKept ? ` \xB7 ${hiddenKept} kept hidden` : ""} \xB7 uses ${usageDays}d`,
536
558
  focused: focus === "skills",
537
559
  width: leftWidth,
538
560
  height: listHeight,
539
561
  rows: skillRows,
540
562
  selected: skillIndex,
541
- empty: "None of your skills load globally. \u{1F389}"
563
+ empty: hiddenKept ? `All reviewed: you keep ${plural(hiddenKept, "skill")} global on purpose. k shows them.` : "None of your skills load globally. \u{1F389}"
542
564
  }
543
565
  ),
544
566
  /* @__PURE__ */ jsx2(
@@ -571,6 +593,7 @@ function CleanupWizard({
571
593
  ["space", focus === "skills" ? "move / keep / delete" : "tick project"],
572
594
  ...focus === "skills" ? [["d", "delete"]] : [],
573
595
  ["tab", "switch side"],
596
+ ...hiddenKept ? [["k", showKept ? "hide kept" : `show kept (${hiddenKept})`]] : [],
574
597
  ["enter", `review (${plural(totalInstalls, "install")})`],
575
598
  ["p", "ask an agent"],
576
599
  ["esc", "cancel"]
@@ -602,7 +625,8 @@ var HELP_TOPICS = [
602
625
  "tab switch lists inside a project",
603
626
  "esc clear the search, then go back to the left pane",
604
627
  "ctrl+r reload ctrl+c quit",
605
- "p in the cleanup wizard: copy a prompt asking an agent to review the skills"
628
+ "p in the cleanup wizard: copy a prompt asking an agent to review the skills",
629
+ "k in the cleanup wizard: show / hide the skills you keep global on purpose"
606
630
  ]
607
631
  },
608
632
  {
@@ -610,7 +634,8 @@ var HELP_TOPICS = [
610
634
  lines: [
611
635
  "\u273B \u2B21 \u25CE \u2124 Claude Code, Cursor, Codex, Zed \u2014 lit when that agent loads the skill here",
612
636
  "\xB2 Cursor reaches it through two folders (it reads several)",
613
- "\u26A0 loaded globally loads in every repo on this machine",
637
+ "\u26A0 loaded globally loads in every repo on this machine, and you haven't decided about it yet",
638
+ "\u2713 global loads in every repo, and you keep it global on purpose (Enter \u2192 Keep global on purpose)",
614
639
  "\u29C9 N copies the same skill reaches agents from N places \u2014 keep one",
615
640
  "\u25CF in sync \xB7 \u2191 update \xB7 \u270E edited \xB7 \u25CB only here",
616
641
  "\u2713 committed in git, so teammates get it \xB1 changed committed, with local edits",
@@ -851,6 +876,7 @@ function buildReviewPrompt(skills, ctx) {
851
876
  `- Location: ${tildify(s.path)}${s.links.length ? ` (also linked from ${s.links.map(tildify).join(", ")})` : ""}`,
852
877
  `- Loaded by: ${s.loadedBy}`,
853
878
  ...s.vendor ? [`- Vendor-managed: ${s.vendor}`] : [],
879
+ ...s.keptGlobal ? ["- I marked this as global on purpose: recommend changing that only for a strong reason (unused, duplicated, low quality)"] : [],
854
880
  `- Usage (Claude Code transcripts only): ${usage}`,
855
881
  `- Installed in projects: ${s.installedIn.length ? s.installedIn.join(", ") : "none"}`,
856
882
  `- Other copies agents can see: ${s.otherCopies.length ? s.otherCopies.join("; ") : "none"}`,
@@ -1470,6 +1496,7 @@ function App() {
1470
1496
  const dupes = (name) => copiesOf(name).length;
1471
1497
  const globalItem = (m) => {
1472
1498
  const inLib = libraryNames.has(m.name);
1499
+ const kept = m.movable && snapshot.keptGlobal.has(m.name);
1473
1500
  const alsoLocal = rows.some((r) => r.installed && r.name === m.name);
1474
1501
  const moveHere = () => confirm(
1475
1502
  `Use ${m.name} only where you add it? It's ${inLib ? "" : "imported into the library, "}added to ${where}, and stops loading globally \u2014 other projects lose it until you add it there. Restorable from Health.`,
@@ -1483,6 +1510,7 @@ function App() {
1483
1510
  );
1484
1511
  const actions = m.movable ? [
1485
1512
  { label: "Choose which repos keep it\u2026", hint: "cleanup wizard", run: () => setWizard({ initialSkill: m.name }) },
1513
+ keepAction(m),
1486
1514
  { label: `Use only in ${where}`, hint: "add here, stop loading everywhere", run: moveHere },
1487
1515
  { label: "Delete", hint: "not kept in Your skills \xB7 restorable from Health", run: () => confirmDeleteGlobal([m]) },
1488
1516
  ...inLib ? [] : [
@@ -1511,9 +1539,9 @@ function App() {
1511
1539
  ...harnessChips(enabledIds.map((id) => ({ id, paths: m.harnesses.includes(id) ? 1 : 0 })), true),
1512
1540
  { text: "", width: 8 },
1513
1541
  {
1514
- text: ` ${dupes(m.name) > 1 ? `\u29C9 ${dupes(m.name)} copies` : m.movable ? "\u26A0 loaded globally" : "vendor"}`,
1542
+ text: ` ${dupes(m.name) > 1 ? `\u29C9 ${dupes(m.name)} copies` : kept ? "\u2713 global" : m.movable ? "\u26A0 loaded globally" : "vendor"}`,
1515
1543
  width: 19,
1516
- color: dupes(m.name) > 1 ? color.red : m.movable ? color.yellow : color.faint
1544
+ color: dupes(m.name) > 1 ? color.red : kept ? color.green : m.movable ? color.yellow : color.faint
1517
1545
  },
1518
1546
  { text: usesText(m.name, root).padStart(4), color: color.muted }
1519
1547
  ]
@@ -1526,7 +1554,7 @@ function App() {
1526
1554
  meta: [
1527
1555
  `loaded in every project by ${m.harnesses.map(harnessName).join(", ")}`,
1528
1556
  `${m.kind}: ${m.origin}`,
1529
- m.movable ? "yours: the cleanup wizard can scope it to projects" : "managed by the vendor"
1557
+ kept ? "yours: kept global on purpose" : m.movable ? "yours: the cleanup wizard can scope it to projects" : "managed by the vendor"
1530
1558
  ],
1531
1559
  description: dupes(m.name) > 1 ? `Loaded ${dupes(m.name)} times here: ${copiesOf(m.name).join(" \xB7 ")}. Agents may see it twice \u2014 keep one.` : sourceGroup(m)?.about
1532
1560
  }
@@ -1580,8 +1608,10 @@ function App() {
1580
1608
  const fromLibrary = local.filter((r) => r.managed || r.state === "untracked copy of library skill");
1581
1609
  const repoOwn = local.filter((r) => !fromLibrary.includes(r));
1582
1610
  const yours = globals.filter((m) => m.movable);
1611
+ const keptYours = yours.filter((m) => snapshot.keptGlobal.has(m.name));
1612
+ const unreviewed = yours.filter((m) => !snapshot.keptGlobal.has(m.name));
1583
1613
  const vendor = globals.filter((m) => !m.movable);
1584
- const cleanupItem = yours.length ? [
1614
+ const cleanupItem = unreviewed.length ? [
1585
1615
  {
1586
1616
  key: "__cleanup",
1587
1617
  search: "clean up global",
@@ -1590,7 +1620,7 @@ function App() {
1590
1620
  key: "__cleanup",
1591
1621
  cells: [
1592
1622
  { text: "\u26A0 ", color: color.yellow },
1593
- { text: `${plural2(yours.length, "skill")} of yours load in every repo \u2014 choose where each should live\u2026`, grow: true, color: color.yellow }
1623
+ { text: `${plural2(unreviewed.length, "skill")} of yours load in every repo \u2014 choose where each should live\u2026`, grow: true, color: color.yellow }
1594
1624
  ]
1595
1625
  },
1596
1626
  primary: { label: "clean up", run: () => setWizard({}) },
@@ -1634,28 +1664,14 @@ function App() {
1634
1664
  ]
1635
1665
  },
1636
1666
  {
1637
- title: `\u26A0 Global \xB7 yours, loaded in every repo (${yours.length})`,
1638
- items: [...cleanupItem, ...yours.map(globalItem)],
1639
- groupActions: (members) => [
1640
- {
1641
- label: `Copy review prompt for all ${members.length}`,
1642
- hint: "for an agent to decide keep / move / delete",
1643
- run: () => copyReviewPrompt(snapshot.machine.filter((m) => members.some((x) => x.skill?.path === m.path)).map(reviewOfGlobal), `These are global skills (group ${members[0]?.group?.label ?? ""}).`)
1644
- },
1645
- {
1646
- label: `Copy all ${members.length} to Your skills`,
1647
- hint: "they keep loading globally",
1648
- run: () => act(() => {
1649
- const added = members.map((m) => importSkill(m.skill.path)).filter((r) => r.status === "added").length;
1650
- return ok(`${plural2(added, "skill")} imported into your library`);
1651
- })
1652
- },
1653
- {
1654
- label: `Delete all ${members.length}`,
1655
- hint: "not kept in Your skills \xB7 restorable from Health",
1656
- run: () => confirmDeleteGlobal(snapshot.machine.filter((m) => members.some((x) => x.skill?.path === m.path)))
1657
- }
1658
- ]
1667
+ title: `\u26A0 Global \xB7 yours, not reviewed, loaded in every repo (${unreviewed.length})`,
1668
+ items: [...cleanupItem, ...unreviewed.map(globalItem)],
1669
+ groupActions: (members) => [...keepGroupActions(members), ...yourGlobalActions(members)]
1670
+ },
1671
+ {
1672
+ title: `\u2713 Global \xB7 yours, on purpose (${keptYours.length})`,
1673
+ items: keptYours.map(globalItem),
1674
+ groupActions: (members) => [...keepGroupActions(members), ...yourGlobalActions(members)]
1659
1675
  },
1660
1676
  {
1661
1677
  title: `Global \xB7 from vendors: plugins, claude.ai, built-in (${vendor.length})`,
@@ -1795,6 +1811,8 @@ function App() {
1795
1811
  }
1796
1812
  function globalSections() {
1797
1813
  const yours = snapshot.machine.filter((m) => m.movable);
1814
+ const unreviewed = yours.filter((m) => !m.broken && !snapshot.keptGlobal.has(m.name));
1815
+ const keptCount = yours.filter((m) => !m.broken && snapshot.keptGlobal.has(m.name)).length;
1798
1816
  const importAll = {
1799
1817
  label: "Copy all of yours to Your skills",
1800
1818
  run: () => {
@@ -1833,12 +1851,16 @@ function App() {
1833
1851
  key: "__cleanup",
1834
1852
  cells: [
1835
1853
  { text: "\u26A0 ", color: color.yellow },
1836
- { text: `Clean up: choose which repos keep each of your ${yours.length} global skills\u2026`, grow: true, color: color.yellow }
1854
+ { text: `Clean up: choose which repos keep each of your ${unreviewed.length} unreviewed global skills\u2026`, grow: true, color: color.yellow }
1837
1855
  ]
1838
1856
  },
1839
1857
  primary: { label: "clean up", run: () => setWizard({}) },
1840
1858
  actions: [{ label: "Open the cleanup wizard", run: () => setWizard({}) }],
1841
- preview: { title: "Clean up global skills", meta: ["every agent reads global skills in every repo"], description: "Install each skill only where it's needed, then stop loading it everywhere. Originals are kept in backup." }
1859
+ preview: {
1860
+ title: "Clean up global skills",
1861
+ meta: ["every agent reads global skills in every repo", ...keptCount ? [`skips the ${keptCount} you keep global on purpose (k shows them)`] : []],
1862
+ description: "Install each skill only where it's needed, keep it global on purpose, or delete it. Originals are kept in backup."
1863
+ }
1842
1864
  };
1843
1865
  const grouped = groups.map(([title, test]) => ({
1844
1866
  title,
@@ -1849,6 +1871,7 @@ function App() {
1849
1871
  run: () => copyReviewPrompt(snapshot.machine.filter((m) => members.some((x) => x.skill?.path === m.path)).map(reviewOfGlobal), `These are global skills (group ${members[0]?.group?.label ?? ""}).`)
1850
1872
  },
1851
1873
  ...title.startsWith("Yours") ? [
1874
+ ...keepGroupActions(members),
1852
1875
  {
1853
1876
  label: `Delete all ${members.length}`,
1854
1877
  hint: "not kept in Your skills \xB7 restorable from Health",
@@ -1879,7 +1902,7 @@ function App() {
1879
1902
  actions.push({ label: "Remove the broken link", run: unload });
1880
1903
  } else {
1881
1904
  if (!inLib) actions.push({ label: "Copy to Your skills", hint: "keeps loading globally", run: importIt });
1882
- if (m.movable) actions.push({ label: "Stop loading it globally", hint: "use it per project instead", run: unload });
1905
+ if (m.movable) actions.push(keepAction(m), { label: "Stop loading it globally", hint: "use it per project instead", run: unload });
1883
1906
  primary = m.movable ? { label: "choose repos\u2026", run: () => setWizard({ initialSkill: m.name }) } : !inLib ? { label: "copy to your skills", run: importIt } : void 0;
1884
1907
  if (m.movable) {
1885
1908
  actions.unshift({ label: "Choose which repos keep it\u2026", hint: "cleanup wizard", run: () => setWizard({ initialSkill: m.name }) });
@@ -1901,6 +1924,7 @@ function App() {
1901
1924
  { text: m.name.padEnd(30).slice(0, 30), color: m.broken ? color.red : void 0 },
1902
1925
  { text: m.broken ? "broken link" : m.origin, grow: true, color: m.broken ? color.red : kindColor[m.kind] },
1903
1926
  ...harnessChips(enabledIds.map((id) => ({ id, paths: m.harnesses.includes(id) ? 1 : 0 })), !m.broken),
1927
+ { text: m.movable && snapshot.keptGlobal.has(m.name) ? " \u2713 kept" : " ", color: color.green },
1904
1928
  { text: inLib ? " \u2713 library" : " ", color: color.green },
1905
1929
  { text: usesText(m.name).padStart(5), color: color.muted }
1906
1930
  ]
@@ -1913,7 +1937,7 @@ function App() {
1913
1937
  meta: [
1914
1938
  m.harnesses.length ? `loaded everywhere by ${m.harnesses.map(harnessName).join(", ")}` : "not loaded by your harnesses",
1915
1939
  `${m.kind}: ${m.origin}`,
1916
- m.movable ? "yours to manage" : "managed by the vendor",
1940
+ m.movable ? snapshot.keptGlobal.has(m.name) ? "yours \xB7 kept global on purpose" : "yours to manage" : "managed by the vendor",
1917
1941
  tildify2(m.path) + (m.links.length ? ` (+ ${m.links.length} link${m.links.length > 1 ? "s" : ""})` : "")
1918
1942
  ],
1919
1943
  description: m.broken ? "Broken link: it points at a folder that no longer exists, so it loads nothing." : void 0
@@ -1952,7 +1976,48 @@ function App() {
1952
1976
  description: "The prompt includes each skill's description, a SKILL.md excerpt, how it was installed (vendor, skills.sh, plugin\u2026), which agents load it, usage per project, duplicates and your project list. Paste it into Claude Code, Cursor or Codex; it replies with keep / move / delete per skill."
1953
1977
  }
1954
1978
  };
1955
- return [{ title: "", items: [...yours.some((m) => !m.broken) ? [launcher] : [], reviewAll] }, ...grouped];
1979
+ return [{ title: "", items: [...unreviewed.length ? [launcher] : [], reviewAll] }, ...grouped];
1980
+ }
1981
+ function markKept(names, keep) {
1982
+ act(() => {
1983
+ setKeepGlobal(names, keep);
1984
+ const label = names.length === 1 ? names[0] : plural2(names.length, "skill");
1985
+ return ok(keep ? `${label} kept global on purpose \u2014 cleanup skips ${names.length === 1 ? "it" : "them"}` : `${label} back in cleanup`);
1986
+ });
1987
+ }
1988
+ function keepAction(m) {
1989
+ return snapshot.keptGlobal.has(m.name) ? { label: "Unmark: not global on purpose", hint: "cleanup asks about it again", run: () => markKept([m.name], false) } : { label: "\u2713 Keep global on purpose", hint: "stops the \u26A0 and skips it in cleanup", run: () => markKept([m.name], true) };
1990
+ }
1991
+ function keepGroupActions(members) {
1992
+ const names = [...new Set(members.flatMap((m) => m.skill ? [m.skill.name] : []))];
1993
+ const unkept = names.filter((n) => !snapshot.keptGlobal.has(n));
1994
+ const kept = names.filter((n) => snapshot.keptGlobal.has(n));
1995
+ return [
1996
+ ...unkept.length ? [{ label: `\u2713 Keep all ${unkept.length} global on purpose`, hint: "cleanup skips them", run: () => markKept(unkept, true) }] : [],
1997
+ ...kept.length ? [{ label: `Unmark all ${kept.length}`, hint: "cleanup asks about them again", run: () => markKept(kept, false) }] : []
1998
+ ];
1999
+ }
2000
+ function yourGlobalActions(members) {
2001
+ return [
2002
+ {
2003
+ label: `Copy review prompt for all ${members.length}`,
2004
+ hint: "for an agent to decide keep / move / delete",
2005
+ run: () => copyReviewPrompt(snapshot.machine.filter((m) => members.some((x) => x.skill?.path === m.path)).map(reviewOfGlobal), `These are global skills (group ${members[0]?.group?.label ?? ""}).`)
2006
+ },
2007
+ {
2008
+ label: `Copy all ${members.length} to Your skills`,
2009
+ hint: "they keep loading globally",
2010
+ run: () => act(() => {
2011
+ const added = members.map((m) => importSkill(m.skill.path)).filter((r) => r.status === "added").length;
2012
+ return ok(`${plural2(added, "skill")} imported into your library`);
2013
+ })
2014
+ },
2015
+ {
2016
+ label: `Delete all ${members.length}`,
2017
+ hint: "not kept in Your skills \xB7 restorable from Health",
2018
+ run: () => confirmDeleteGlobal(snapshot.machine.filter((m) => members.some((x) => x.skill?.path === m.path)))
2019
+ }
2020
+ ];
1956
2021
  }
1957
2022
  function confirmDeleteGlobal(skills) {
1958
2023
  if (skills.length === 0) return;
@@ -2001,7 +2066,8 @@ function App() {
2001
2066
  usesByProject: u.byProject,
2002
2067
  installedIn: projectsWith(m.name),
2003
2068
  otherCopies: snapshot.machine.filter((x) => x.name === m.name && x.path !== m.path).map((x) => `${x.kind} ${x.origin} (${tildify2(x.path)})`),
2004
- inYourSkills: lib ? `yes, v${lib.version}` : null
2069
+ inYourSkills: lib ? `yes, v${lib.version}` : null,
2070
+ keptGlobal: m.movable && snapshot.keptGlobal.has(m.name)
2005
2071
  };
2006
2072
  }
2007
2073
  function reviewOfLibrary(l) {
@@ -2496,6 +2562,7 @@ function App() {
2496
2562
  CleanupWizard,
2497
2563
  {
2498
2564
  skills: yours,
2565
+ kept: snapshot.keptGlobal,
2499
2566
  projects: snapshot.projects,
2500
2567
  usesIn: (skill, project) => usage ? usage.byProject.get(project)?.get(skill) ?? 0 : null,
2501
2568
  usesTotal: (skill) => usage ? usage.total.get(skill) ?? 0 : null,
@@ -2524,7 +2591,12 @@ function App() {
2524
2591
  reload();
2525
2592
  setToast({
2526
2593
  ok: r.problems.length === 0,
2527
- text: `${plural2(r.unloaded, "skill")} moved into projects (${plural2(r.installs, "install")})${r.deleted ? ` \xB7 ${r.deleted} deleted` : ""}${r.problems.length ? ` \xB7 ${r.problems.length} kept global: ${r.problems[0]}` : ""}`
2594
+ text: [
2595
+ r.unloaded || r.installs ? `${plural2(r.unloaded, "skill")} moved into projects (${plural2(r.installs, "install")})` : "",
2596
+ r.deleted ? `${r.deleted} deleted` : "",
2597
+ r.kept ? `${plural2(r.kept, "skill")} kept global on purpose` : "",
2598
+ r.problems.length ? `${r.problems.length} left global: ${r.problems[0]}` : ""
2599
+ ].filter(Boolean).join(" \xB7 ")
2528
2600
  });
2529
2601
  }
2530
2602
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skilllib",
3
- "version": "1.0.2",
3
+ "version": "1.1.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",