skilllib 1.0.1 → 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
|
|
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";
|
|
@@ -550,8 +563,7 @@ function librarySkills() {
|
|
|
550
563
|
return skillDirsIn(libraryDir()).map((dir) => ({
|
|
551
564
|
name: basename3(dir),
|
|
552
565
|
description: readSkillInfo(dir).description,
|
|
553
|
-
dir
|
|
554
|
-
hash: treeHash(dir) ?? ""
|
|
566
|
+
dir
|
|
555
567
|
}));
|
|
556
568
|
}
|
|
557
569
|
function librarySkillDir(name) {
|
|
@@ -850,7 +862,9 @@ function unloadGlobal(path, links = [], { requireLibrary = true } = {}) {
|
|
|
850
862
|
const broken = !existsSync7(join8(path, "SKILL.md"));
|
|
851
863
|
if (requireLibrary && !broken && latestVersion(name) === null) return { ok: false, reason: "import it into the library first" };
|
|
852
864
|
for (const link2 of links) if (isLink(link2)) stash(link2, "global-backup");
|
|
853
|
-
|
|
865
|
+
const movedTo = stash(path, "global-backup");
|
|
866
|
+
if (!globalSkillDirs().some((d) => entryExists(join8(d, name)))) setKeepGlobal([name], false);
|
|
867
|
+
return { ok: true, movedTo };
|
|
854
868
|
}
|
|
855
869
|
function createSkill(name, description) {
|
|
856
870
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) return { ok: false, reason: "use lowercase letters, digits and dashes" };
|
|
@@ -1499,6 +1513,28 @@ function link(args) {
|
|
|
1499
1513
|
if (res.blocked.length) warn(`${label}git tracks ${res.blocked.join(", ")}; re-run with --allow-tracked to link there too`);
|
|
1500
1514
|
}
|
|
1501
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
|
+
}
|
|
1502
1538
|
|
|
1503
1539
|
export {
|
|
1504
1540
|
userHome,
|
|
@@ -1523,6 +1559,8 @@ export {
|
|
|
1523
1559
|
harnessesChosen,
|
|
1524
1560
|
setHarnesses,
|
|
1525
1561
|
allowTrackedLinks,
|
|
1562
|
+
keptGlobal,
|
|
1563
|
+
setKeepGlobal,
|
|
1526
1564
|
machineSkills,
|
|
1527
1565
|
libraryOrigins,
|
|
1528
1566
|
librarySkills,
|
|
@@ -1538,7 +1576,6 @@ export {
|
|
|
1538
1576
|
syncProject,
|
|
1539
1577
|
updateProject,
|
|
1540
1578
|
importSkill,
|
|
1541
|
-
projectsUsing,
|
|
1542
1579
|
deleteLibrarySkill,
|
|
1543
1580
|
deleteGlobal,
|
|
1544
1581
|
unloadGlobal,
|
|
@@ -1569,5 +1606,6 @@ export {
|
|
|
1569
1606
|
usage,
|
|
1570
1607
|
show,
|
|
1571
1608
|
harnessesCommand,
|
|
1572
|
-
link
|
|
1609
|
+
link,
|
|
1610
|
+
globalCommand
|
|
1573
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-
|
|
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-
|
|
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-
|
|
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,
|
|
@@ -27,7 +28,6 @@ import {
|
|
|
27
28
|
machineSkills,
|
|
28
29
|
projectOfFactory,
|
|
29
30
|
projectStatus,
|
|
30
|
-
projectsUsing,
|
|
31
31
|
readConfig,
|
|
32
32
|
readSkillInfo,
|
|
33
33
|
relinkDependency,
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
scanUsage,
|
|
38
38
|
setHarnesses,
|
|
39
39
|
setHidden,
|
|
40
|
+
setKeepGlobal,
|
|
40
41
|
skilllibHome,
|
|
41
42
|
summarize,
|
|
42
43
|
syncProject,
|
|
@@ -47,7 +48,7 @@ import {
|
|
|
47
48
|
versionHistory,
|
|
48
49
|
visibilityOf,
|
|
49
50
|
visibleProjects
|
|
50
|
-
} from "./chunk-
|
|
51
|
+
} from "./chunk-HLCJQVHB.js";
|
|
51
52
|
|
|
52
53
|
// src/tui/index.tsx
|
|
53
54
|
import { render } from "ink";
|
|
@@ -250,7 +251,8 @@ function loadSnapshot() {
|
|
|
250
251
|
name: s.name,
|
|
251
252
|
description: s.description,
|
|
252
253
|
origin: origins[s.name] ?? inferred(s.name),
|
|
253
|
-
|
|
254
|
+
// Managed status entries are the manifest, already read above; no need to re-read every manifest per skill.
|
|
255
|
+
projects: projects.filter((p) => statusOf(p).some((x) => x.managed && x.name === s.name)).sort(),
|
|
254
256
|
version: latestVersion(s.name)?.version ?? null
|
|
255
257
|
}));
|
|
256
258
|
return {
|
|
@@ -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,
|
|
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
|
|
374
|
-
const
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
431
|
-
if (
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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"}`,
|
|
@@ -1163,7 +1189,8 @@ function App() {
|
|
|
1163
1189
|
const edit = async (file) => {
|
|
1164
1190
|
const editor = process.env.VISUAL || process.env.EDITOR || (process.platform === "win32" ? "notepad" : "vi");
|
|
1165
1191
|
await suspendTerminal(() => {
|
|
1166
|
-
spawnSync2(`${editor} "${file}"`, { shell: true, stdio: "inherit" });
|
|
1192
|
+
if (process.platform === "win32") spawnSync2(`${editor} "${file}"`, { shell: true, stdio: "inherit" });
|
|
1193
|
+
else spawnSync2("/bin/sh", ["-c", `${editor} "$1"`, "sh", file], { stdio: "inherit" });
|
|
1167
1194
|
});
|
|
1168
1195
|
reload();
|
|
1169
1196
|
say(`Saved ${tildify2(file)}`);
|
|
@@ -1469,6 +1496,7 @@ function App() {
|
|
|
1469
1496
|
const dupes = (name) => copiesOf(name).length;
|
|
1470
1497
|
const globalItem = (m) => {
|
|
1471
1498
|
const inLib = libraryNames.has(m.name);
|
|
1499
|
+
const kept = m.movable && snapshot.keptGlobal.has(m.name);
|
|
1472
1500
|
const alsoLocal = rows.some((r) => r.installed && r.name === m.name);
|
|
1473
1501
|
const moveHere = () => confirm(
|
|
1474
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.`,
|
|
@@ -1482,6 +1510,7 @@ function App() {
|
|
|
1482
1510
|
);
|
|
1483
1511
|
const actions = m.movable ? [
|
|
1484
1512
|
{ label: "Choose which repos keep it\u2026", hint: "cleanup wizard", run: () => setWizard({ initialSkill: m.name }) },
|
|
1513
|
+
keepAction(m),
|
|
1485
1514
|
{ label: `Use only in ${where}`, hint: "add here, stop loading everywhere", run: moveHere },
|
|
1486
1515
|
{ label: "Delete", hint: "not kept in Your skills \xB7 restorable from Health", run: () => confirmDeleteGlobal([m]) },
|
|
1487
1516
|
...inLib ? [] : [
|
|
@@ -1510,9 +1539,9 @@ function App() {
|
|
|
1510
1539
|
...harnessChips(enabledIds.map((id) => ({ id, paths: m.harnesses.includes(id) ? 1 : 0 })), true),
|
|
1511
1540
|
{ text: "", width: 8 },
|
|
1512
1541
|
{
|
|
1513
|
-
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"}`,
|
|
1514
1543
|
width: 19,
|
|
1515
|
-
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
|
|
1516
1545
|
},
|
|
1517
1546
|
{ text: usesText(m.name, root).padStart(4), color: color.muted }
|
|
1518
1547
|
]
|
|
@@ -1525,7 +1554,7 @@ function App() {
|
|
|
1525
1554
|
meta: [
|
|
1526
1555
|
`loaded in every project by ${m.harnesses.map(harnessName).join(", ")}`,
|
|
1527
1556
|
`${m.kind}: ${m.origin}`,
|
|
1528
|
-
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"
|
|
1529
1558
|
],
|
|
1530
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
|
|
1531
1560
|
}
|
|
@@ -1579,8 +1608,10 @@ function App() {
|
|
|
1579
1608
|
const fromLibrary = local.filter((r) => r.managed || r.state === "untracked copy of library skill");
|
|
1580
1609
|
const repoOwn = local.filter((r) => !fromLibrary.includes(r));
|
|
1581
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));
|
|
1582
1613
|
const vendor = globals.filter((m) => !m.movable);
|
|
1583
|
-
const cleanupItem =
|
|
1614
|
+
const cleanupItem = unreviewed.length ? [
|
|
1584
1615
|
{
|
|
1585
1616
|
key: "__cleanup",
|
|
1586
1617
|
search: "clean up global",
|
|
@@ -1589,7 +1620,7 @@ function App() {
|
|
|
1589
1620
|
key: "__cleanup",
|
|
1590
1621
|
cells: [
|
|
1591
1622
|
{ text: "\u26A0 ", color: color.yellow },
|
|
1592
|
-
{ text: `${plural2(
|
|
1623
|
+
{ text: `${plural2(unreviewed.length, "skill")} of yours load in every repo \u2014 choose where each should live\u2026`, grow: true, color: color.yellow }
|
|
1593
1624
|
]
|
|
1594
1625
|
},
|
|
1595
1626
|
primary: { label: "clean up", run: () => setWizard({}) },
|
|
@@ -1633,28 +1664,14 @@ function App() {
|
|
|
1633
1664
|
]
|
|
1634
1665
|
},
|
|
1635
1666
|
{
|
|
1636
|
-
title: `\u26A0 Global \xB7 yours, loaded in every repo (${
|
|
1637
|
-
items: [...cleanupItem, ...
|
|
1638
|
-
groupActions: (members) => [
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
{
|
|
1645
|
-
label: `Copy all ${members.length} to Your skills`,
|
|
1646
|
-
hint: "they keep loading globally",
|
|
1647
|
-
run: () => act(() => {
|
|
1648
|
-
const added = members.map((m) => importSkill(m.skill.path)).filter((r) => r.status === "added").length;
|
|
1649
|
-
return ok(`${plural2(added, "skill")} imported into your library`);
|
|
1650
|
-
})
|
|
1651
|
-
},
|
|
1652
|
-
{
|
|
1653
|
-
label: `Delete all ${members.length}`,
|
|
1654
|
-
hint: "not kept in Your skills \xB7 restorable from Health",
|
|
1655
|
-
run: () => confirmDeleteGlobal(snapshot.machine.filter((m) => members.some((x) => x.skill?.path === m.path)))
|
|
1656
|
-
}
|
|
1657
|
-
]
|
|
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)]
|
|
1658
1675
|
},
|
|
1659
1676
|
{
|
|
1660
1677
|
title: `Global \xB7 from vendors: plugins, claude.ai, built-in (${vendor.length})`,
|
|
@@ -1794,6 +1811,8 @@ function App() {
|
|
|
1794
1811
|
}
|
|
1795
1812
|
function globalSections() {
|
|
1796
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;
|
|
1797
1816
|
const importAll = {
|
|
1798
1817
|
label: "Copy all of yours to Your skills",
|
|
1799
1818
|
run: () => {
|
|
@@ -1832,12 +1851,16 @@ function App() {
|
|
|
1832
1851
|
key: "__cleanup",
|
|
1833
1852
|
cells: [
|
|
1834
1853
|
{ text: "\u26A0 ", color: color.yellow },
|
|
1835
|
-
{ text: `Clean up: choose which repos keep each of your ${
|
|
1854
|
+
{ text: `Clean up: choose which repos keep each of your ${unreviewed.length} unreviewed global skills\u2026`, grow: true, color: color.yellow }
|
|
1836
1855
|
]
|
|
1837
1856
|
},
|
|
1838
1857
|
primary: { label: "clean up", run: () => setWizard({}) },
|
|
1839
1858
|
actions: [{ label: "Open the cleanup wizard", run: () => setWizard({}) }],
|
|
1840
|
-
preview: {
|
|
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
|
+
}
|
|
1841
1864
|
};
|
|
1842
1865
|
const grouped = groups.map(([title, test]) => ({
|
|
1843
1866
|
title,
|
|
@@ -1848,6 +1871,7 @@ function App() {
|
|
|
1848
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 ?? ""}).`)
|
|
1849
1872
|
},
|
|
1850
1873
|
...title.startsWith("Yours") ? [
|
|
1874
|
+
...keepGroupActions(members),
|
|
1851
1875
|
{
|
|
1852
1876
|
label: `Delete all ${members.length}`,
|
|
1853
1877
|
hint: "not kept in Your skills \xB7 restorable from Health",
|
|
@@ -1878,7 +1902,7 @@ function App() {
|
|
|
1878
1902
|
actions.push({ label: "Remove the broken link", run: unload });
|
|
1879
1903
|
} else {
|
|
1880
1904
|
if (!inLib) actions.push({ label: "Copy to Your skills", hint: "keeps loading globally", run: importIt });
|
|
1881
|
-
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 });
|
|
1882
1906
|
primary = m.movable ? { label: "choose repos\u2026", run: () => setWizard({ initialSkill: m.name }) } : !inLib ? { label: "copy to your skills", run: importIt } : void 0;
|
|
1883
1907
|
if (m.movable) {
|
|
1884
1908
|
actions.unshift({ label: "Choose which repos keep it\u2026", hint: "cleanup wizard", run: () => setWizard({ initialSkill: m.name }) });
|
|
@@ -1900,6 +1924,7 @@ function App() {
|
|
|
1900
1924
|
{ text: m.name.padEnd(30).slice(0, 30), color: m.broken ? color.red : void 0 },
|
|
1901
1925
|
{ text: m.broken ? "broken link" : m.origin, grow: true, color: m.broken ? color.red : kindColor[m.kind] },
|
|
1902
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 },
|
|
1903
1928
|
{ text: inLib ? " \u2713 library" : " ", color: color.green },
|
|
1904
1929
|
{ text: usesText(m.name).padStart(5), color: color.muted }
|
|
1905
1930
|
]
|
|
@@ -1912,7 +1937,7 @@ function App() {
|
|
|
1912
1937
|
meta: [
|
|
1913
1938
|
m.harnesses.length ? `loaded everywhere by ${m.harnesses.map(harnessName).join(", ")}` : "not loaded by your harnesses",
|
|
1914
1939
|
`${m.kind}: ${m.origin}`,
|
|
1915
|
-
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",
|
|
1916
1941
|
tildify2(m.path) + (m.links.length ? ` (+ ${m.links.length} link${m.links.length > 1 ? "s" : ""})` : "")
|
|
1917
1942
|
],
|
|
1918
1943
|
description: m.broken ? "Broken link: it points at a folder that no longer exists, so it loads nothing." : void 0
|
|
@@ -1951,7 +1976,48 @@ function App() {
|
|
|
1951
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."
|
|
1952
1977
|
}
|
|
1953
1978
|
};
|
|
1954
|
-
return [{ title: "", items: [...
|
|
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
|
+
];
|
|
1955
2021
|
}
|
|
1956
2022
|
function confirmDeleteGlobal(skills) {
|
|
1957
2023
|
if (skills.length === 0) return;
|
|
@@ -2000,7 +2066,8 @@ function App() {
|
|
|
2000
2066
|
usesByProject: u.byProject,
|
|
2001
2067
|
installedIn: projectsWith(m.name),
|
|
2002
2068
|
otherCopies: snapshot.machine.filter((x) => x.name === m.name && x.path !== m.path).map((x) => `${x.kind} ${x.origin} (${tildify2(x.path)})`),
|
|
2003
|
-
inYourSkills: lib ? `yes, v${lib.version}` : null
|
|
2069
|
+
inYourSkills: lib ? `yes, v${lib.version}` : null,
|
|
2070
|
+
keptGlobal: m.movable && snapshot.keptGlobal.has(m.name)
|
|
2004
2071
|
};
|
|
2005
2072
|
}
|
|
2006
2073
|
function reviewOfLibrary(l) {
|
|
@@ -2495,6 +2562,7 @@ function App() {
|
|
|
2495
2562
|
CleanupWizard,
|
|
2496
2563
|
{
|
|
2497
2564
|
skills: yours,
|
|
2565
|
+
kept: snapshot.keptGlobal,
|
|
2498
2566
|
projects: snapshot.projects,
|
|
2499
2567
|
usesIn: (skill, project) => usage ? usage.byProject.get(project)?.get(skill) ?? 0 : null,
|
|
2500
2568
|
usesTotal: (skill) => usage ? usage.total.get(skill) ?? 0 : null,
|
|
@@ -2523,7 +2591,12 @@ function App() {
|
|
|
2523
2591
|
reload();
|
|
2524
2592
|
setToast({
|
|
2525
2593
|
ok: r.problems.length === 0,
|
|
2526
|
-
text:
|
|
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 ")
|
|
2527
2600
|
});
|
|
2528
2601
|
}
|
|
2529
2602
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skilllib",
|
|
3
|
-
"version": "1.0
|
|
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",
|