skilllib 1.13.0 → 1.14.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 +12 -8
- package/dist/{chunk-C6TSR7NT.js → chunk-XMXC2XM2.js} +352 -58
- package/dist/index.js +17 -7
- package/dist/json.d.cts +38 -1
- package/dist/json.d.ts +38 -1
- package/dist/{tui-3MBVYSVM.js → tui-N6NRJYLZ.js} +7 -13
- package/package.json +1 -1
- package/skills/skilllib/SKILL.md +6 -4
package/README.md
CHANGED
|
@@ -193,10 +193,10 @@ These are the issues `skilllib doctor` reports, with the same fixes and choices.
|
|
|
193
193
|
|
|
194
194
|
A skill's pin goes in one of two places:
|
|
195
195
|
|
|
196
|
-
-
|
|
197
|
-
-
|
|
196
|
+
- **Your personal pins (`~/.skilllib/pins/`).** The default. One file per repo, matched by its `origin` remote, so every clone and worktree of the repo shares it. skilllib keeps the copies and links out of git through the repo's `.git/info/exclude` (only your clone; the repo's `.gitignore` doesn't change). Nothing shows up in `git status`. A repo that already has `skilllib.json` still gets new skills here.
|
|
197
|
+
- **`skilllib.json` in the repo (shared).** Only when you share a skill with the team: `skilllib add <name> --shared`, or *Pin it for the team* in the app. Commit it with the skill folders, and teammates get that skill.
|
|
198
198
|
|
|
199
|
-
`
|
|
199
|
+
`--personal` moves a shared pin back to yours, without changing its version. A copy git already has can't become personal. A skill pinned in both places uses the repo's pin; doctor offers to drop yours (a choice: yours would keep the skill if the team unpinned it). The app tags a personal skill `personal v2`.
|
|
200
200
|
|
|
201
201
|
**On another machine:** keep `~/.skilllib/pins/` and `~/.skilllib/library/` in git (a dotfiles repo, say). Clone a repo there and run `skilllib sync`: your personal skills come back, out of git again. A repo without a remote keeps its pins by folder, so they stay on that machine until it gets one: adding a remote (or renaming it) keeps them. Only a repo's top folder can have personal pins, not a monorepo package's.
|
|
202
202
|
|
|
@@ -249,7 +249,7 @@ Every plugin issue is *your call*: turning plugins on or off, or updating them,
|
|
|
249
249
|
|
|
250
250
|
The details list its skills with their uses, what else it brings, where it's installed and its version. Its actions:
|
|
251
251
|
|
|
252
|
-
- **Turn it on / off**: `claude plugin enable|disable --scope <scope>`, run in its repo for a project or local install. A plugin synced from claude.ai is switched by its `"<name>@synced"` line in `~/.claude/settings.json`; so is any plugin when the `claude` command isn't available.
|
|
252
|
+
- **Turn it on / off**: `skilllib plugin off <id>` and `skilllib plugin on <id>` (`name@marketplace`). That switches the copy that loads in every repo and leaves a per-repo install on. The same switch is `claude plugin enable|disable --scope <scope>`, run in its repo for a project or local install. A plugin synced from claude.ai is switched by its `"<name>@synced"` line in `~/.claude/settings.json`; so is any plugin when the `claude` command isn't available.
|
|
253
253
|
- **Update to vN**, when its marketplace (as Claude Code last fetched it) declares a newer version or commit: `claude plugin update`. If the marketplace asks to run a command, skilllib doesn't confirm it for you: it tells you to run the update yourself.
|
|
254
254
|
- **Uninstall**: `claude plugin uninstall --keep-data`, where it's installed. Settings › Backups reinstalls it, in the same repo for a project install.
|
|
255
255
|
- **Replace *plugin* with library skills…** copies all its skills into your library (grouped under the plugin), adds them to the repos you tick, and uninstalls the plugin. A plugin can't be half removed, so it's all its skills at once; the confirmation says what else it brings that goes too. If the `claude` command isn't available, skilllib turns the plugin off instead and tells you to finish with `/plugin uninstall`. Plugins synced from claude.ai can't be removed from here: skilllib copies their skills, turns the plugin off, and tells you to remove it on claude.ai.
|
|
@@ -409,6 +409,8 @@ skilllib new <name> [desc] create a skill
|
|
|
409
409
|
|
|
410
410
|
skilllib projects your projects and their skills
|
|
411
411
|
skilllib usage [--days N] which skills Claude Code used (a plugin's copy counts for the skill's name)
|
|
412
|
+
skilllib check [--days N] what loaded skills cost on every prompt, and which had no use
|
|
413
|
+
skilllib plugin off|on <id> turn a Claude Code plugin off or on (it stays installed)
|
|
412
414
|
skilllib doctor [--fix] find (and fix) problems
|
|
413
415
|
skilllib apply <id> [choice] apply one problem's fix, or the choice you pick (ids from status or doctor --json)
|
|
414
416
|
skilllib restore [name] put back something skilllib moved out (copies moved together come back together)
|
|
@@ -424,7 +426,7 @@ Flags: `--personal` / `--shared` (for `add`: pin in your personal pins or in the
|
|
|
424
426
|
|
|
425
427
|
A command exits 1 when it couldn't do all you asked: a skill skipped (not in the library, or local edits in the way), links git-tracked folders blocked, a link something else is in the way of (a file or another copy), an unknown `tidy` name or `--keep` folder, or (with `--all`, and `projects`) a repo left out because its `skilllib.json` can't be read. What it did do is still done, and listed. An unknown or misspelled flag is an error too, so a typo never runs silently. With `--json`, an error that stops a command prints `{"error": "…"}` on stdout as well as the message on stderr. The commands SKILL.md lists as messages-only print nothing on stdout otherwise: read their exit code.
|
|
426
428
|
|
|
427
|
-
**`--json`** prints one line of JSON on stdout, for scripts and agents. Messages and warnings go to stderr. It works with `status`, `list`, `show`, `projects`, `outdated`, `usage`, `doctor`, `apply`, `add`, `remove`, `sync`, `update`, `import`, `global move` and `
|
|
429
|
+
**`--json`** prints one line of JSON on stdout, for scripts and agents. Messages and warnings go to stderr. It works with `status`, `list`, `show`, `projects`, `outdated`, `usage`, `doctor`, `apply`, `add`, `remove`, `sync`, `update`, `import`, `global move`, `agent-skill`, `check` and `plugin`. The output is shaped to keep agents fast and cheap:
|
|
428
430
|
|
|
429
431
|
- **`status --json`** answers everything about the repo in one call:
|
|
430
432
|
- the repo's skills, with their source, version, state and git state
|
|
@@ -455,11 +457,13 @@ A field is only removed or changed in a major version.
|
|
|
455
457
|
|
|
456
458
|
| Path | What |
|
|
457
459
|
|---|---|
|
|
458
|
-
| `<repo>/skilllib.json` |
|
|
460
|
+
| `<repo>/skilllib.json` | Skills you shared with the team (`--shared`). **Commit it** with the skill folders. Absent until then |
|
|
459
461
|
| `~/.skilllib/pins/` | Your personal pins, one file per repo (by its remote). Keep it in git with your library to use them on other machines. |
|
|
460
462
|
| `~/.skilllib/library/` | Your skills (master copies). Worth putting under git. |
|
|
461
463
|
| `~/.skilllib/store/` | Every version of every skill, immutable |
|
|
462
464
|
| `~/.skilllib/config.json` | Your agents, project folders, hidden projects, skills you keep global |
|
|
465
|
+
| `~/.skilllib/projects.json` | Repos skilllib has seen. Rewritten only when that list changes |
|
|
466
|
+
| `~/.skilllib/usage-cache.json` | Parsed Claude Code transcripts, so the next `status` doesn't read them again. Safe to delete |
|
|
463
467
|
| `~/.skilllib/trash/`, `global-backup/`, `tidy-backup/`, `edit-backup/` | Everything skilllib removed, restorable from Settings › Backups |
|
|
464
468
|
| `~/.skilllib/plugin-backup.json` | Plugins skilllib uninstalled (with their scope and repo), so Settings › Backups can reinstall them |
|
|
465
469
|
| `~/.skilllib/review-prompt.md` | The last review prompt you copied |
|
|
@@ -469,11 +473,11 @@ Environment variables: `SKILLLIB_HOME` moves `~/.skilllib`, `$VISUAL` / `$EDITOR
|
|
|
469
473
|
|
|
470
474
|
## FAQ
|
|
471
475
|
|
|
472
|
-
**Does it change my repos?** Only when you tell it to: adding, removing, linking or tidying a skill
|
|
476
|
+
**Does it change my repos?** Only when you tell it to: adding, removing, linking or tidying a skill. That updates your `~/.skilllib/pins/` and the repo's `.git/info/exclude`, or `skilllib.json` when you share a skill with the team. It asks before linking a shared skill into a folder git tracks, and before tidy changes a committed file. It never deletes the only copy of a repo's own skill.
|
|
473
477
|
|
|
474
478
|
**Can I share skills with my team?** Commit `skilllib.json` along with the skill folders (`.agents/skills`, and `.claude/skills` if you use Claude Code). Or keep `~/.skilllib/library` in a git repo of its own (the rest of `~/.skilllib` is personal: config, backups, the version store), and run `skilllib import` on teammates' machines from a clone of it.
|
|
475
479
|
|
|
476
|
-
**Do I have to commit `skilllib.json`?**
|
|
480
|
+
**Do I have to commit `skilllib.json`?** Only for a skill you shared with the team (`skilllib add <name> --shared`). Everything else stays in your personal pins, out of git. See [Personal or shared pins](#personal-or-shared-pins).
|
|
477
481
|
|
|
478
482
|
**Why does usage only count Claude Code?** It's the only agent whose transcripts skilllib reads. Grok keeps sessions under `~/.grok/sessions`, and Cursor doesn't keep them locally. Codex's format hasn't been tested yet. That's why "unused" is a strong hint, not proof.
|
|
479
483
|
|
|
@@ -434,15 +434,25 @@ function indexFile(name) {
|
|
|
434
434
|
return join4(storeDir(name), "versions.json");
|
|
435
435
|
}
|
|
436
436
|
function versionHistory(name) {
|
|
437
|
-
|
|
437
|
+
const hit = historyCache.get(name);
|
|
438
|
+
if (hit) return hit;
|
|
439
|
+
const history = readState(indexFile(name)) ?? [];
|
|
440
|
+
historyCache.set(name, history);
|
|
441
|
+
return history;
|
|
438
442
|
}
|
|
439
443
|
function versionDir(name, version) {
|
|
440
444
|
return join4(storeDir(name), String(version));
|
|
441
445
|
}
|
|
442
446
|
var latestCache = /* @__PURE__ */ new Map();
|
|
447
|
+
var historyCache = /* @__PURE__ */ new Map();
|
|
443
448
|
function forgetLatest(name) {
|
|
444
|
-
if (name)
|
|
445
|
-
|
|
449
|
+
if (name) {
|
|
450
|
+
latestCache.delete(name);
|
|
451
|
+
historyCache.delete(name);
|
|
452
|
+
} else {
|
|
453
|
+
latestCache.clear();
|
|
454
|
+
historyCache.clear();
|
|
455
|
+
}
|
|
446
456
|
}
|
|
447
457
|
function latestVersion(name) {
|
|
448
458
|
if (latestCache.has(name)) return latestCache.get(name) ?? null;
|
|
@@ -459,7 +469,9 @@ function latestVersion(name) {
|
|
|
459
469
|
if (existsSync3(versionDir(name, next.version))) throw new Error(`${versionDir(name, next.version)} exists but ${indexFile(name)} doesn't list it: fix the index before adding versions`);
|
|
460
470
|
mkdirSync2(storeDir(name), { recursive: true });
|
|
461
471
|
copySkill(source, versionDir(name, next.version));
|
|
462
|
-
|
|
472
|
+
const recorded = [...history, next];
|
|
473
|
+
writeState(indexFile(name), recorded);
|
|
474
|
+
historyCache.set(name, recorded);
|
|
463
475
|
result = next;
|
|
464
476
|
}
|
|
465
477
|
}
|
|
@@ -585,26 +597,63 @@ function normalizeRemote(url) {
|
|
|
585
597
|
}
|
|
586
598
|
var repos = /* @__PURE__ */ new Map();
|
|
587
599
|
var changedAt = (path) => statSync3(path, { throwIfNoEntry: false })?.mtimeMs ?? -1;
|
|
600
|
+
function gitCommon(root) {
|
|
601
|
+
const gitPath = join5(root, ".git");
|
|
602
|
+
const st = statSync3(gitPath, { throwIfNoEntry: false });
|
|
603
|
+
if (!st) return null;
|
|
604
|
+
if (st.isDirectory()) return existsSync4(join5(gitPath, "HEAD")) ? gitPath : null;
|
|
605
|
+
if (!st.isFile()) return null;
|
|
606
|
+
const gitdir = readFileSync3(gitPath, "utf-8").match(/^gitdir:\s*(.+?)\s*$/m)?.[1];
|
|
607
|
+
if (!gitdir) return null;
|
|
608
|
+
const dir = resolve3(root, gitdir);
|
|
609
|
+
if (!existsSync4(dir)) return null;
|
|
610
|
+
let common = dir;
|
|
611
|
+
try {
|
|
612
|
+
const resolved = resolve3(dir, readFileSync3(join5(dir, "commondir"), "utf-8").trim());
|
|
613
|
+
if (existsSync4(resolved)) common = resolved;
|
|
614
|
+
} catch {
|
|
615
|
+
}
|
|
616
|
+
return existsSync4(join5(common, "HEAD")) ? common : null;
|
|
617
|
+
}
|
|
618
|
+
function remoteUrls(configPath) {
|
|
619
|
+
let text;
|
|
620
|
+
try {
|
|
621
|
+
text = readFileSync3(configPath, "utf-8");
|
|
622
|
+
} catch {
|
|
623
|
+
return [];
|
|
624
|
+
}
|
|
625
|
+
if (/^\s*\[include/m.test(text)) return null;
|
|
626
|
+
const urls = [];
|
|
627
|
+
let section = "";
|
|
628
|
+
for (const line of text.split(/\r?\n/)) {
|
|
629
|
+
const header = line.match(/^\s*\[([^\]]+)\]\s*$/);
|
|
630
|
+
if (header) {
|
|
631
|
+
const remote = header[1].match(/^remote\s+"(.+)"$/) ?? header[1].match(/^remote\s+(\S+)$/);
|
|
632
|
+
section = remote?.[1] ?? "";
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
const url = section ? line.match(/^\s*url\s*=\s*(.*?)\s*$/) : null;
|
|
636
|
+
if (url) urls.push([`remote.${section}.url`, url[1]]);
|
|
637
|
+
}
|
|
638
|
+
return urls;
|
|
639
|
+
}
|
|
588
640
|
function repoOf(root) {
|
|
589
641
|
const real = realPath(root);
|
|
590
642
|
const hit = repos.get(real);
|
|
591
643
|
if (hit && (hit.config ? changedAt(hit.config) === hit.seen : !existsSync4(join5(root, ".git")))) return hit.repo;
|
|
592
|
-
const
|
|
593
|
-
if (!
|
|
644
|
+
const commonPath = gitCommon(root);
|
|
645
|
+
if (!commonPath) {
|
|
594
646
|
repos.set(real, { repo: null, config: null, seen: 0 });
|
|
595
647
|
return null;
|
|
596
648
|
}
|
|
597
|
-
const common = realNative(
|
|
649
|
+
const common = realNative(commonPath) ?? commonPath;
|
|
598
650
|
const folder = basename2(common) === ".git" ? dirname2(common) : common;
|
|
599
651
|
const config = join5(common, "config");
|
|
600
652
|
const seen = changedAt(config);
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
const remote = url ? normalizeRemote(url) : void 0;
|
|
606
|
-
repo = { file: pinsFileFor(root, remote, folder), folder, common, ...remote ? { remote } : {}, root };
|
|
607
|
-
}
|
|
653
|
+
const urls = remoteUrls(config) ?? (gitIn(root, ["config", "--get-regexp", "^remote\\..*\\.url$"]) ?? "").split("\n").filter(Boolean).map((l) => [l.slice(0, l.indexOf(" ")), l.slice(l.indexOf(" ") + 1)]);
|
|
654
|
+
const url = (urls.find(([k]) => k === "remote.origin.url") ?? urls[0])?.[1];
|
|
655
|
+
const remote = url ? normalizeRemote(url) : void 0;
|
|
656
|
+
const repo = { file: pinsFileFor(root, remote, folder), folder, common, ...remote ? { remote } : {}, root };
|
|
608
657
|
repos.set(real, { repo, config, seen });
|
|
609
658
|
return repo;
|
|
610
659
|
}
|
|
@@ -696,7 +745,10 @@ function isProjectCandidate(root) {
|
|
|
696
745
|
function rememberProjects(roots) {
|
|
697
746
|
const incoming = roots.map((r) => resolve3(r)).filter((r) => isProjectCandidate(r) && !isAgentWorktree(r));
|
|
698
747
|
const all = onePerFolder([...knownProjects(), ...incoming]).sort();
|
|
699
|
-
|
|
748
|
+
const path = projectsFile();
|
|
749
|
+
const prev = readState(path);
|
|
750
|
+
if (Array.isArray(prev) && prev.length === all.length && prev.every((p, i) => p === all[i])) return;
|
|
751
|
+
writeState(path, all);
|
|
700
752
|
}
|
|
701
753
|
function readableRoots(roots) {
|
|
702
754
|
const unreadable = [];
|
|
@@ -1249,7 +1301,9 @@ function trackBlocker(root, name) {
|
|
|
1249
1301
|
return `${MANIFEST_FILE} can't be read`;
|
|
1250
1302
|
}
|
|
1251
1303
|
const stuck = firstStuck(root, dependencyDirs(root, name, recorded));
|
|
1252
|
-
|
|
1304
|
+
if (stuck) return blockerSaid(root, stuck);
|
|
1305
|
+
if (!recorded && !personalPinsFile(root) && gitRepoAbove(root)) return "only the top folder of a git repo can have personal pins";
|
|
1306
|
+
return null;
|
|
1253
1307
|
}
|
|
1254
1308
|
function pinsSkill(root, name) {
|
|
1255
1309
|
try {
|
|
@@ -1598,10 +1652,20 @@ function addSkill(root, name, { force = false, version, allowTracked = false, pe
|
|
|
1598
1652
|
...moved ? { moved } : {}
|
|
1599
1653
|
};
|
|
1600
1654
|
}
|
|
1655
|
+
function gitRepoAbove(dir) {
|
|
1656
|
+
let at = resolve6(dir);
|
|
1657
|
+
while (true) {
|
|
1658
|
+
if (existsSync7(join8(at, ".git", "HEAD"))) return true;
|
|
1659
|
+
const parent = dirname3(at);
|
|
1660
|
+
if (parent === at) return false;
|
|
1661
|
+
at = parent;
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1601
1664
|
function pinsWhere(root, recorded, personal) {
|
|
1602
|
-
const want = personal ?? (recorded ? !!recorded.personal :
|
|
1603
|
-
if (
|
|
1604
|
-
|
|
1665
|
+
const want = personal ?? (recorded ? !!recorded.personal : !!personalPinsFile(root));
|
|
1666
|
+
if (want && !personalPinsFile(root)) return "only the top folder of a git repo can have personal pins";
|
|
1667
|
+
if (personal === void 0 && !recorded && !personalPinsFile(root) && gitRepoAbove(root)) return "only the top folder of a git repo can have personal pins";
|
|
1668
|
+
return want;
|
|
1605
1669
|
}
|
|
1606
1670
|
var personalOf = (manifest) => Object.fromEntries(Object.entries(manifest.skills).filter(([, d]) => d.personal));
|
|
1607
1671
|
function removeSkill(root, name, { force = false } = {}) {
|
|
@@ -1924,9 +1988,9 @@ function refreshAgentSkill() {
|
|
|
1924
1988
|
}
|
|
1925
1989
|
|
|
1926
1990
|
// src/commands.ts
|
|
1927
|
-
import { existsSync as existsSync15, readFileSync as readFileSync7, rmSync as
|
|
1991
|
+
import { existsSync as existsSync15, readFileSync as readFileSync7, rmSync as rmSync8 } from "fs";
|
|
1928
1992
|
import { parseArgs as parseFlags } from "util";
|
|
1929
|
-
import { basename as basename11, join as
|
|
1993
|
+
import { basename as basename11, join as join17, resolve as resolve9, sep as sep10 } from "path";
|
|
1930
1994
|
|
|
1931
1995
|
// src/git.ts
|
|
1932
1996
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
@@ -1945,11 +2009,13 @@ function git(root, args, input) {
|
|
|
1945
2009
|
}
|
|
1946
2010
|
function gitInfo(root, extraDirs = []) {
|
|
1947
2011
|
let inRepo;
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
2012
|
+
if (existsSync9(join10(root, ".git"))) inRepo = "";
|
|
2013
|
+
else {
|
|
2014
|
+
try {
|
|
2015
|
+
inRepo = execFileSync3("git", ["rev-parse", "--show-prefix"], { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
2016
|
+
} catch {
|
|
2017
|
+
return null;
|
|
2018
|
+
}
|
|
1953
2019
|
}
|
|
1954
2020
|
const real = (p) => {
|
|
1955
2021
|
try {
|
|
@@ -2535,7 +2601,7 @@ function intoLibrary(path, force = false) {
|
|
|
2535
2601
|
return r.name;
|
|
2536
2602
|
}
|
|
2537
2603
|
var pinsOf = (where, personal) => personal ? "your personal pins" : `${where}'s skilllib.json`;
|
|
2538
|
-
var newPinsOf = (root, where) => pinsOf(where,
|
|
2604
|
+
var newPinsOf = (root, where) => pinsOf(where, personalPinsFile(root) !== null);
|
|
2539
2605
|
function track(root, name) {
|
|
2540
2606
|
const c = addSkill(root, name);
|
|
2541
2607
|
if (c.action === "skipped") throw new Error(`${name}: ${c.reason}`);
|
|
@@ -3278,13 +3344,13 @@ function repoIssues(root, ctx) {
|
|
|
3278
3344
|
id: `local:${root}:${s.name}`,
|
|
3279
3345
|
severity: "suggestion",
|
|
3280
3346
|
title: `${s.name} in ${where} isn't in your library`,
|
|
3281
|
-
detail:
|
|
3347
|
+
detail: `Import it into the library to reuse it in other projects and keep it backed up. The repo then tracks it in ${newPinsOf(root, where)}.`,
|
|
3282
3348
|
at: rows(root, [s.name]),
|
|
3283
3349
|
choices: [
|
|
3284
3350
|
{
|
|
3285
3351
|
label: "Import into the library",
|
|
3286
3352
|
// Tracking it writes skilllib.json.
|
|
3287
|
-
hint: `copies it into Your skills${gitNote(root, [join13(root, MANIFEST_FILE)])}`,
|
|
3353
|
+
hint: `copies it into Your skills${personalPinsFile(root) ? "" : gitNote(root, [join13(root, MANIFEST_FILE)])}`,
|
|
3288
3354
|
run: () => {
|
|
3289
3355
|
intoLibrary(s.path);
|
|
3290
3356
|
track(root, s.name);
|
|
@@ -3341,7 +3407,7 @@ function repoIssues(root, ctx) {
|
|
|
3341
3407
|
id: `npx:${root}:${s.name}`,
|
|
3342
3408
|
severity: "suggestion",
|
|
3343
3409
|
title: `${s.name} in ${where} came from npx skills, not your library`,
|
|
3344
|
-
detail: `Installed with \`npx skills add\`${s.source ? ` (${s.source})` : ""}. Import it into your library to reuse it in other projects and keep it backed up${teams ? `; ${where}'s copy stays as it is (${committed(s) ? "committed" : `${untrackable(s)}, so skilllib can't track it here`})` : `; ${where} then tracks it
|
|
3410
|
+
detail: `Installed with \`npx skills add\`${s.source ? ` (${s.source})` : ""}. Import it into your library to reuse it in other projects and keep it backed up${teams ? `; ${where}'s copy stays as it is (${committed(s) ? "committed" : `${untrackable(s)}, so skilllib can't track it here`})` : `; ${where} then tracks it in ${newPinsOf(root, where)}`}.`,
|
|
3345
3411
|
at: rows(root, [s.name]),
|
|
3346
3412
|
choices: [
|
|
3347
3413
|
{
|
|
@@ -3612,8 +3678,8 @@ function libraryFor(currentProject) {
|
|
|
3612
3678
|
}
|
|
3613
3679
|
|
|
3614
3680
|
// src/usage.ts
|
|
3615
|
-
import { createReadStream, existsSync as existsSync13, readdirSync as readdirSync10, statSync as statSync6 } from "fs";
|
|
3616
|
-
import { join as join14 } from "path";
|
|
3681
|
+
import { createReadStream, existsSync as existsSync13, mkdirSync as mkdirSync9, readdirSync as readdirSync10, renameSync as renameSync4, rmSync as rmSync6, statSync as statSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
3682
|
+
import { dirname as dirname7, join as join14 } from "path";
|
|
3617
3683
|
import { createInterface } from "readline";
|
|
3618
3684
|
var BUILTIN_COMMANDS = /* @__PURE__ */ new Set([
|
|
3619
3685
|
"add-dir",
|
|
@@ -3715,21 +3781,58 @@ function transcriptFiles(sinceMs) {
|
|
|
3715
3781
|
});
|
|
3716
3782
|
});
|
|
3717
3783
|
}
|
|
3784
|
+
function usageCacheFile() {
|
|
3785
|
+
return join14(skilllibHome(), "usage-cache.json");
|
|
3786
|
+
}
|
|
3787
|
+
function readUsageCache() {
|
|
3788
|
+
const data = readJson(usageCacheFile());
|
|
3789
|
+
return data?.files && typeof data.files === "object" ? data : { files: {} };
|
|
3790
|
+
}
|
|
3791
|
+
function writeUsageCache(cache) {
|
|
3792
|
+
const path = usageCacheFile();
|
|
3793
|
+
const temp = `${path}.${process.pid}.tmp`;
|
|
3794
|
+
try {
|
|
3795
|
+
mkdirSync9(dirname7(path), { recursive: true });
|
|
3796
|
+
writeFileSync6(temp, JSON.stringify(cache));
|
|
3797
|
+
renameSync4(temp, path);
|
|
3798
|
+
} catch {
|
|
3799
|
+
rmSync6(temp, { force: true });
|
|
3800
|
+
}
|
|
3801
|
+
}
|
|
3802
|
+
async function usesIn(file) {
|
|
3803
|
+
const uses = [];
|
|
3804
|
+
const lines = createInterface({ input: createReadStream(file), crlfDelay: Infinity });
|
|
3805
|
+
for await (const line of lines) uses.push(...usesFromTranscriptLine(line));
|
|
3806
|
+
return uses;
|
|
3807
|
+
}
|
|
3718
3808
|
async function scanUsage(days) {
|
|
3719
3809
|
const sinceMs = Date.now() - days * 24 * 60 * 60 * 1e3;
|
|
3810
|
+
const files = transcriptFiles(sinceMs);
|
|
3811
|
+
const cache = readUsageCache();
|
|
3812
|
+
let dirty = false;
|
|
3813
|
+
for (const file of Object.keys(cache.files)) {
|
|
3814
|
+
if (!files.includes(file) && !existsSync13(file)) {
|
|
3815
|
+
delete cache.files[file];
|
|
3816
|
+
dirty = true;
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
3720
3819
|
const seen = /* @__PURE__ */ new Map();
|
|
3721
|
-
for (const file of
|
|
3820
|
+
for (const file of files) {
|
|
3722
3821
|
try {
|
|
3723
|
-
const
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3822
|
+
const st = statSync6(file);
|
|
3823
|
+
const mtimeMs = Math.round(st.mtimeMs);
|
|
3824
|
+
const hit = cache.files[file];
|
|
3825
|
+
const uses = hit && hit.size === st.size && hit.mtimeMs === mtimeMs && Array.isArray(hit.uses) ? hit.uses : await usesIn(file);
|
|
3826
|
+
if (uses !== hit?.uses) {
|
|
3827
|
+
cache.files[file] = { size: st.size, mtimeMs, uses };
|
|
3828
|
+
dirty = true;
|
|
3728
3829
|
}
|
|
3830
|
+
for (const use of uses) if (Date.parse(use.at) >= sinceMs) seen.set(use.key, use);
|
|
3729
3831
|
} catch {
|
|
3730
3832
|
continue;
|
|
3731
3833
|
}
|
|
3732
3834
|
}
|
|
3835
|
+
if (dirty) writeUsageCache(cache);
|
|
3733
3836
|
return [...seen.values()];
|
|
3734
3837
|
}
|
|
3735
3838
|
var skillOf = (key) => key.split(":").pop();
|
|
@@ -3761,7 +3864,7 @@ function usesByProject(uses, projectOf) {
|
|
|
3761
3864
|
|
|
3762
3865
|
// src/install.ts
|
|
3763
3866
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
3764
|
-
import { createWriteStream, existsSync as existsSync14, lstatSync as lstatSync5, mkdirSync as
|
|
3867
|
+
import { createWriteStream, existsSync as existsSync14, lstatSync as lstatSync5, mkdirSync as mkdirSync10, mkdtempSync, readdirSync as readdirSync11, rmSync as rmSync7, statSync as statSync7 } from "fs";
|
|
3765
3868
|
import { tmpdir as tmpdir2 } from "os";
|
|
3766
3869
|
import { basename as basename9, join as join15, resolve as resolve8, sep as sep9 } from "path";
|
|
3767
3870
|
import { Readable } from "stream";
|
|
@@ -3851,16 +3954,16 @@ async function downloadGitHub(owner, repo) {
|
|
|
3851
3954
|
try {
|
|
3852
3955
|
res = await fetch(url, { redirect: "follow" });
|
|
3853
3956
|
} catch (err) {
|
|
3854
|
-
|
|
3957
|
+
rmSync7(dest, { recursive: true, force: true });
|
|
3855
3958
|
throw new Error(`couldn't download ${owner}/${repo} (${err instanceof Error ? err.message : err})`);
|
|
3856
3959
|
}
|
|
3857
3960
|
if (!res.ok || !res.body) {
|
|
3858
|
-
|
|
3961
|
+
rmSync7(dest, { recursive: true, force: true });
|
|
3859
3962
|
throw new Error(res.status === 404 ? `${owner}/${repo} not found on GitHub` : `couldn't download ${owner}/${repo} (${res.status})`);
|
|
3860
3963
|
}
|
|
3861
3964
|
const host = new URL(res.url).hostname;
|
|
3862
3965
|
if (host !== "codeload.github.com" && host !== "github.com") {
|
|
3863
|
-
|
|
3966
|
+
rmSync7(dest, { recursive: true, force: true });
|
|
3864
3967
|
throw new Error(`refusing to download ${owner}/${repo} from ${host}`);
|
|
3865
3968
|
}
|
|
3866
3969
|
let bytes = 0;
|
|
@@ -3882,11 +3985,11 @@ async function downloadGitHub(owner, repo) {
|
|
|
3882
3985
|
);
|
|
3883
3986
|
execFileSync5("tar", ["-xzf", archive, "-C", dest], { stdio: "ignore" });
|
|
3884
3987
|
} catch (err) {
|
|
3885
|
-
|
|
3988
|
+
rmSync7(dest, { recursive: true, force: true });
|
|
3886
3989
|
if (err instanceof Error && /larger than|couldn't download/.test(err.message)) throw err;
|
|
3887
3990
|
throw new Error(`couldn't extract ${owner}/${repo}`);
|
|
3888
3991
|
}
|
|
3889
|
-
|
|
3992
|
+
rmSync7(archive, { force: true });
|
|
3890
3993
|
const top = readdirSync11(dest);
|
|
3891
3994
|
const root = top.length === 1 && statSync7(join15(dest, top[0])).isDirectory() ? join15(dest, top[0]) : dest;
|
|
3892
3995
|
return { tmp: dest, root };
|
|
@@ -3904,19 +4007,19 @@ function installGlobal(name, { force = false } = {}) {
|
|
|
3904
4007
|
return { ok: false, reason: `${name} is already global, with edits that aren't in your library (pass --force to replace them)` };
|
|
3905
4008
|
}
|
|
3906
4009
|
if (there !== latest.hash) {
|
|
3907
|
-
if (isLink(dest))
|
|
3908
|
-
|
|
4010
|
+
if (isLink(dest)) rmSync7(dest, { force: true });
|
|
4011
|
+
mkdirSync10(primary, { recursive: true });
|
|
3909
4012
|
copySkill(versionDir(name, latest.version), dest);
|
|
3910
4013
|
}
|
|
3911
4014
|
const wrote = [dest];
|
|
3912
4015
|
for (const dir of links) {
|
|
3913
4016
|
const link2 = join15(dir, name);
|
|
3914
|
-
if (isLink(link2) && realpathOrNull(link2) === null)
|
|
4017
|
+
if (isLink(link2) && realpathOrNull(link2) === null) rmSync7(link2, { force: true });
|
|
3915
4018
|
else if (entryExists(link2)) {
|
|
3916
4019
|
if (realpathOrNull(link2) === realpathOrNull(dest)) wrote.push(link2);
|
|
3917
4020
|
continue;
|
|
3918
4021
|
}
|
|
3919
|
-
|
|
4022
|
+
mkdirSync10(dir, { recursive: true });
|
|
3920
4023
|
linkDir(realpathOrNull(dest) ?? dest, join15(realpathOrNull(dir) ?? dir, name));
|
|
3921
4024
|
wrote.push(link2);
|
|
3922
4025
|
}
|
|
@@ -3964,6 +4067,87 @@ async function resolveInstallDir(spec) {
|
|
|
3964
4067
|
return { tmp: got.tmp, dirs, source, many: !spec.skill && dirs.length > 1 };
|
|
3965
4068
|
}
|
|
3966
4069
|
|
|
4070
|
+
// src/check.ts
|
|
4071
|
+
import { join as join16 } from "path";
|
|
4072
|
+
function promptTokens(name, description) {
|
|
4073
|
+
return Math.max(1, Math.ceil((name.length + 1 + description.length) / 4));
|
|
4074
|
+
}
|
|
4075
|
+
function freshLabel(state, version, latest) {
|
|
4076
|
+
if (state === "update available") return `v${version} \u2192 v${latest}`;
|
|
4077
|
+
if (state === "edited locally, update available") return `edited here, library has v${latest}`;
|
|
4078
|
+
if (state === "edited locally") return "edited here";
|
|
4079
|
+
if (state === "out of sync") return "not the pinned version";
|
|
4080
|
+
if (state === "folder missing") return "folder missing";
|
|
4081
|
+
if (state === "ok" || state === "repo skill, in library" || state === "untracked copy of library skill") return "current";
|
|
4082
|
+
if (state === "untracked, differs from library" || state === "repo skill, differs from library") return "differs from your library";
|
|
4083
|
+
return "not in your library";
|
|
4084
|
+
}
|
|
4085
|
+
function libraryFresh(path, name) {
|
|
4086
|
+
const lib = join16(libraryDir(), name);
|
|
4087
|
+
if (!isSkillDir(lib)) return "not in your library";
|
|
4088
|
+
const here = treeHash(path);
|
|
4089
|
+
return here && here === treeHash(lib) ? "matches your library" : "differs from your library";
|
|
4090
|
+
}
|
|
4091
|
+
var settled = /* @__PURE__ */ new Set(["current", "matches your library", "vendor", "skilllib"]);
|
|
4092
|
+
function byWeight(rows2) {
|
|
4093
|
+
const rank = (r) => (r.uses === 0 ? 0 : 1) * 2 + (settled.has(r.fresh) ? 1 : 0);
|
|
4094
|
+
return [...rows2].sort((a, b) => rank(a) - rank(b) || b.everyPrompt - a.everyPrompt || a.name.localeCompare(b.name));
|
|
4095
|
+
}
|
|
4096
|
+
function tokenSum(rows2, where) {
|
|
4097
|
+
return rows2.filter((r) => !where || r.where === where).reduce((n, r) => n + r.everyPrompt, 0);
|
|
4098
|
+
}
|
|
4099
|
+
function rough(n) {
|
|
4100
|
+
const rounded = n >= 100 ? Math.round(n / 100) * 100 : n;
|
|
4101
|
+
return String(rounded).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
4102
|
+
}
|
|
4103
|
+
function checkBrief(rows2, ctx) {
|
|
4104
|
+
const global = rows2.filter((r) => r.where === "global");
|
|
4105
|
+
const repo = rows2.filter((r) => r.where === "repo");
|
|
4106
|
+
const globalTokens = tokenSum(global);
|
|
4107
|
+
const unused = rows2.filter((r) => r.uses === 0);
|
|
4108
|
+
const unusedGlobal = global.filter((r) => r.uses === 0 && r.lever !== "skilllib");
|
|
4109
|
+
const unusedTokens = tokenSum(unusedGlobal);
|
|
4110
|
+
const n = (count) => `${count} skill${count === 1 ? "" : "s"}`;
|
|
4111
|
+
const lines = [
|
|
4112
|
+
`${n(rows2.length)}. ~${rough(globalTokens)} tokens of names and descriptions in every prompt${ctx.repo ? `, plus ~${rough(tokenSum(repo))} from this repo` : ""}.`,
|
|
4113
|
+
`${unused.length} had no Claude Code use in ${ctx.days} days. A zero is a hint: Cursor and Codex aren't in these transcripts.`
|
|
4114
|
+
];
|
|
4115
|
+
if (!globalTokens) return [...lines, "Nothing global is loading."];
|
|
4116
|
+
lines.push(
|
|
4117
|
+
unusedTokens * 2 >= globalTokens ? "Heavy. More than half of every prompt's skill list had no use." : unusedTokens * 4 >= globalTokens ? "Mixed. A good share of every prompt is skills with no use." : "Fine. Most of what loads has been used."
|
|
4118
|
+
);
|
|
4119
|
+
const plugins = Object.entries(Object.groupBy(global.filter((r) => r.lever === "plugin"), (r) => r.via ?? r.name)).map(([id, skills = []]) => ({
|
|
4120
|
+
id,
|
|
4121
|
+
tokens: tokenSum(skills),
|
|
4122
|
+
idle: tokenSum(skills.filter((s) => s.uses === 0)),
|
|
4123
|
+
skills: skills.length,
|
|
4124
|
+
used: skills.filter((s) => s.uses > 0).length
|
|
4125
|
+
})).filter((p) => p.idle > 0 && p.idle * 2 >= p.tokens).sort((a, b) => b.tokens - a.tokens);
|
|
4126
|
+
const yours = unusedGlobal.filter((r) => r.lever === "yours");
|
|
4127
|
+
const synced = unusedGlobal.filter((r) => r.lever === "claude.ai");
|
|
4128
|
+
const drop = plugins.reduce((n2, p) => n2 + p.tokens, 0) + tokenSum(yours) + tokenSum(synced);
|
|
4129
|
+
if (drop > 0) lines.push(`You can drop ~${rough(drop)} tokens.`);
|
|
4130
|
+
for (const p of plugins.slice(0, 3)) {
|
|
4131
|
+
lines.push(
|
|
4132
|
+
p.used ? `~${rough(p.tokens)} is ${p.id} (${p.skills} skills, ${p.used} used). Turning the plugin off drops all of it, including ${p.used === 1 ? "that one" : "those"}.` : `~${rough(p.tokens)} is ${p.id} (${p.skills} skills, no use). Turn the plugin off.`
|
|
4133
|
+
);
|
|
4134
|
+
}
|
|
4135
|
+
if (plugins.length > 3) lines.push(`${plugins.length - 3} more plugins are in the same spot.`);
|
|
4136
|
+
if (plugins.length) lines.push("Turn a Claude Code plugin off with skilllib plugin off <id>. Cursor plugins turn off in Cursor.");
|
|
4137
|
+
if (yours.length) {
|
|
4138
|
+
const names = yours.sort((a, b) => b.everyPrompt - a.everyPrompt).slice(0, 5).map((r) => r.name);
|
|
4139
|
+
const more = yours.length > 5 ? `, and ${yours.length - 5} more` : "";
|
|
4140
|
+
lines.push(`~${rough(tokenSum(yours))} are your own global skills with no use (${names.join(", ")}${more}). Move them into the repos that need them, or delete them.`);
|
|
4141
|
+
}
|
|
4142
|
+
if (synced.length) lines.push(`~${rough(tokenSum(synced))} are claude.ai skills with no use. Turn them off on claude.ai.`);
|
|
4143
|
+
const builtins = unusedGlobal.filter((r) => r.lever === "built-in");
|
|
4144
|
+
if (builtins.length) {
|
|
4145
|
+
const who = [...new Set(builtins.map((r) => r.via ?? "the agent"))].join(", ");
|
|
4146
|
+
lines.push(`~${rough(tokenSum(builtins))} are ${who} built-ins with no use. They ship with the agent. skilllib can't unload them.`);
|
|
4147
|
+
}
|
|
4148
|
+
return lines;
|
|
4149
|
+
}
|
|
4150
|
+
|
|
3967
4151
|
// src/move.ts
|
|
3968
4152
|
import { basename as basename10 } from "path";
|
|
3969
4153
|
function moveGlobalSkills(skills, roots, {
|
|
@@ -4116,7 +4300,7 @@ function requireProject() {
|
|
|
4116
4300
|
if (!isProjectCandidate(findProjectRoot())) {
|
|
4117
4301
|
const top = findProjectRoot();
|
|
4118
4302
|
if (realPath(top) !== realPath(userHome())) throw new Error(`${tildify(top)} is skilllib's or an agent's own folder, not a project. Run this inside a project.`);
|
|
4119
|
-
const stray = existsSync15(
|
|
4303
|
+
const stray = existsSync15(join17(userHome(), MANIFEST_FILE)) ? `, though ${tildify(join17(userHome(), MANIFEST_FILE))} makes it look like one` : "";
|
|
4120
4304
|
throw new Error(`Your home folder isn't a project${stray}: skills added here would load in every repo. Run this inside a project.`);
|
|
4121
4305
|
}
|
|
4122
4306
|
throw new Error("Not in a git repo. Run this inside a project (or `git init` first).");
|
|
@@ -4300,7 +4484,7 @@ function add(args) {
|
|
|
4300
4484
|
const pins = readManifest(root).skills;
|
|
4301
4485
|
const done = changes.filter((c) => c.action !== "skipped" && pins[c.name]);
|
|
4302
4486
|
if (!args.json && args.personal === void 0 && done.some((c) => c.action === "installed" && !c.moved && pins[c.name].personal))
|
|
4303
|
-
info(dim(`Pinned
|
|
4487
|
+
info(dim(`Pinned for you (${tildify(personalPinsDir())}), kept out of git. To share it with the team: skilllib add <name> --shared, then commit skilllib.json and the skill folders.`));
|
|
4304
4488
|
rememberProjects([root]);
|
|
4305
4489
|
}
|
|
4306
4490
|
function remove(args) {
|
|
@@ -4384,7 +4568,7 @@ function importSkills(args) {
|
|
|
4384
4568
|
success(`${name} ${dim(result === "unchanged" ? "already in library" : result === "added" ? "added to library" : "library updated")}`);
|
|
4385
4569
|
const root = resolve9(dir, "..", "..", "..");
|
|
4386
4570
|
const dep = projectHere(root) === root ? readManifest(root).skills[name] : void 0;
|
|
4387
|
-
if (dep && realpathOrNull(
|
|
4571
|
+
if (dep && realpathOrNull(join17(root, dep.dir ?? PROJECT_SKILLS_DIR, name)) === realpathOrNull(dir) && dep.hash !== treeHash(dir)) {
|
|
4388
4572
|
const change = addSkill(root, name);
|
|
4389
4573
|
if (change.action !== "skipped") info(dim(` ${basename11(root)} now depends on ${name} v${change.to}`));
|
|
4390
4574
|
}
|
|
@@ -4402,7 +4586,7 @@ function scan(args) {
|
|
|
4402
4586
|
const library = new Set(librarySkills().map((s) => s.name));
|
|
4403
4587
|
const skillNames = (root) => [
|
|
4404
4588
|
// Not a folder that links out of the repo: what's in it isn't the repo's (as in status).
|
|
4405
|
-
.../* @__PURE__ */ new Set([...projectDirsInUse(enabledHarnesses()).filter((d) => !notWritable(root, d)).flatMap((d) => skillDirsIn(
|
|
4589
|
+
.../* @__PURE__ */ new Set([...projectDirsInUse(enabledHarnesses()).filter((d) => !notWritable(root, d)).flatMap((d) => skillDirsIn(join17(root, d))).map((d) => basename11(d)), ...nestedSkills(root).map((s) => s.name)])
|
|
4406
4590
|
];
|
|
4407
4591
|
const withSkills = found.filter((root) => skillNames(root).length > 0);
|
|
4408
4592
|
info(`Scanned ${roots.map((r) => tildify(r)).join(", ")}: ${found.length} project${found.length === 1 ? "" : "s"}, ${withSkills.length} with skills
|
|
@@ -4466,7 +4650,7 @@ function newSkill(args) {
|
|
|
4466
4650
|
if (!r.ok) {
|
|
4467
4651
|
throw new Error(`Can't create ${name}: ${r.reason}`);
|
|
4468
4652
|
}
|
|
4469
|
-
success(`Created ${tildify(
|
|
4653
|
+
success(`Created ${tildify(join17(r.dir, "SKILL.md"))}`);
|
|
4470
4654
|
}
|
|
4471
4655
|
function restore(args) {
|
|
4472
4656
|
const backups = [...listBackups(), ...pluginBackups()];
|
|
@@ -4587,7 +4771,7 @@ async function usage(args) {
|
|
|
4587
4771
|
info(`No skill usage in Claude Code transcripts from the last ${args.days} days.`);
|
|
4588
4772
|
return;
|
|
4589
4773
|
}
|
|
4590
|
-
info(`Skill usage, last ${args.days} days ${dim(`(from ${tildify(
|
|
4774
|
+
info(`Skill usage, last ${args.days} days ${dim(`(from ${tildify(join17(claudeDir(), "projects"))})`)}
|
|
4591
4775
|
`);
|
|
4592
4776
|
table(
|
|
4593
4777
|
summaries.map((s) => ({
|
|
@@ -4603,9 +4787,117 @@ async function usage(args) {
|
|
|
4603
4787
|
warn(`Library skills unused in the last ${args.days} days: ${unused.join(", ")}`);
|
|
4604
4788
|
}
|
|
4605
4789
|
}
|
|
4790
|
+
var LEVER = { plugin: "plugin", "built-in": "built-in", "claude.ai": "claude.ai", system: "system", skilllib: "skilllib" };
|
|
4791
|
+
async function check(args) {
|
|
4792
|
+
const root = projectHere();
|
|
4793
|
+
const usage2 = await usageBySkill(args.days);
|
|
4794
|
+
const kept = keptGlobal();
|
|
4795
|
+
const machine = machineSkills().filter((m) => !m.broken);
|
|
4796
|
+
const loaded = root ? skillsLoadedIn(root, machine) : machine;
|
|
4797
|
+
const local = root ? projectStatus(root) : [];
|
|
4798
|
+
const localByName = new Map(local.map((s) => [s.name, s]));
|
|
4799
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4800
|
+
const rows2 = [];
|
|
4801
|
+
for (const g of loaded) {
|
|
4802
|
+
seen.add(g.name);
|
|
4803
|
+
const here = localByName.get(g.name);
|
|
4804
|
+
const u = usage2.get(g.name);
|
|
4805
|
+
const repoFresh = here && here.state !== "folder missing" ? freshLabel(here.state, here.version, here.latest) : null;
|
|
4806
|
+
const lever = LEVER[g.kind] ?? "yours";
|
|
4807
|
+
rows2.push({
|
|
4808
|
+
name: g.name,
|
|
4809
|
+
where: "global",
|
|
4810
|
+
description: g.description,
|
|
4811
|
+
everyPrompt: promptTokens(g.name, g.description),
|
|
4812
|
+
uses: u?.uses ?? 0,
|
|
4813
|
+
lastUsed: u ? u.lastUsed.slice(0, 10) : null,
|
|
4814
|
+
projects: [...u?.projects ?? []].map((p) => basename11(p)),
|
|
4815
|
+
fresh: lever === "yours" ? repoFresh ?? libraryFresh(g.path, g.name) : lever,
|
|
4816
|
+
path: g.path,
|
|
4817
|
+
lever,
|
|
4818
|
+
...lever !== "yours" && lever !== "skilllib" ? { via: g.origin } : {},
|
|
4819
|
+
...here ? { alsoInRepo: true } : {},
|
|
4820
|
+
...kept.has(g.name) ? { keptGlobal: true } : {}
|
|
4821
|
+
});
|
|
4822
|
+
}
|
|
4823
|
+
for (const s of local) {
|
|
4824
|
+
if (seen.has(s.name) || s.state === "folder missing") continue;
|
|
4825
|
+
const u = usage2.get(s.name);
|
|
4826
|
+
const { description } = readSkillInfo(s.path);
|
|
4827
|
+
rows2.push({
|
|
4828
|
+
name: s.name,
|
|
4829
|
+
where: "repo",
|
|
4830
|
+
description,
|
|
4831
|
+
everyPrompt: promptTokens(s.name, description),
|
|
4832
|
+
uses: u?.uses ?? 0,
|
|
4833
|
+
lastUsed: u ? u.lastUsed.slice(0, 10) : null,
|
|
4834
|
+
projects: [...u?.projects ?? []].map((p) => basename11(p)),
|
|
4835
|
+
fresh: freshLabel(s.state, s.version, s.latest),
|
|
4836
|
+
path: s.path,
|
|
4837
|
+
lever: "repo"
|
|
4838
|
+
});
|
|
4839
|
+
}
|
|
4840
|
+
const skills = byWeight(rows2);
|
|
4841
|
+
const repoName = root ? basename11(root) : null;
|
|
4842
|
+
const brief = checkBrief(skills, { days: args.days, repo: repoName });
|
|
4843
|
+
const report = {
|
|
4844
|
+
days: args.days,
|
|
4845
|
+
repo: repoName,
|
|
4846
|
+
everyPrompt: { global: tokenSum(skills, "global"), repo: tokenSum(skills, "repo") },
|
|
4847
|
+
note: "Uses are Claude Code transcripts only, so 0 means it wasn't seen. everyPrompt is a rough token count (chars/4) of the name and description, which sit in every prompt. The rest of SKILL.md loads only when the skill is used. brief is the report.",
|
|
4848
|
+
brief,
|
|
4849
|
+
skills: skills.map(({ lastUsed: lastUsed2, projects: projects2, via, alsoInRepo, keptGlobal: keptGlobal2, ...rest }) => ({
|
|
4850
|
+
...rest,
|
|
4851
|
+
...lastUsed2 ? { lastUsed: lastUsed2 } : {},
|
|
4852
|
+
...projects2.length ? { projects: projects2 } : {},
|
|
4853
|
+
...via ? { via } : {},
|
|
4854
|
+
...alsoInRepo ? { alsoInRepo } : {},
|
|
4855
|
+
...keptGlobal2 ? { keptGlobal: keptGlobal2 } : {}
|
|
4856
|
+
}))
|
|
4857
|
+
};
|
|
4858
|
+
if (args.json) return json("check", report);
|
|
4859
|
+
if (!skills.length) return info("No skills loaded.");
|
|
4860
|
+
info(brief.join("\n"));
|
|
4861
|
+
}
|
|
4862
|
+
function pluginCommand(args) {
|
|
4863
|
+
const [action, id, extra] = args.positional;
|
|
4864
|
+
if (action !== "off" && action !== "on" || !id || extra) throw new Error("Usage: skilllib plugin off|on <id>");
|
|
4865
|
+
const on = action === "on";
|
|
4866
|
+
const targets = pluginTargets(id);
|
|
4867
|
+
const results = targets.map((p) => setPluginOn(p, on));
|
|
4868
|
+
const ok = results.every((r) => r.ok);
|
|
4869
|
+
const message = [...results.map((r) => r.message), stillOnElsewhere(targets)].filter(Boolean).join(" ");
|
|
4870
|
+
if (!ok) process.exitCode = 1;
|
|
4871
|
+
const body = { id: targets[0].id, on, ok, message };
|
|
4872
|
+
if (args.json) return json("plugin", body);
|
|
4873
|
+
if (ok) success(message);
|
|
4874
|
+
else error(message);
|
|
4875
|
+
}
|
|
4876
|
+
function pluginTargets(id) {
|
|
4877
|
+
const claude2 = installedPlugins().filter((p) => p.agent === "claude-code");
|
|
4878
|
+
const exact = claude2.filter((p) => p.id === id);
|
|
4879
|
+
const matches = exact.length ? exact : claude2.filter((p) => pluginNameOf(p.id) === id);
|
|
4880
|
+
if (!matches.length) {
|
|
4881
|
+
const cursor = cursorPlugins().some((p) => p.id === id || pluginNameOf(p.id) === id);
|
|
4882
|
+
throw new Error(cursor ? `${id} is a Cursor plugin. Turn it off in Cursor (Settings \u2192 Plugins).` : `No Claude Code plugin ${id}.`);
|
|
4883
|
+
}
|
|
4884
|
+
const ids = [...new Set(matches.map((p) => p.id))];
|
|
4885
|
+
if (ids.length > 1) throw new Error(`Several plugins match ${id}: ${ids.join(", ")}. Pass the id.`);
|
|
4886
|
+
const everywhere = matches.filter((p) => !p.projectPath);
|
|
4887
|
+
if (everywhere.length) return everywhere;
|
|
4888
|
+
if (matches.length === 1) return matches;
|
|
4889
|
+
const where = matches.map((p) => `${basename11(p.projectPath)} (${p.scope})`).join(", ");
|
|
4890
|
+
throw new Error(`${matches[0].id} is installed in more than one repo (${where}). This command switches the copy that loads in every repo, and these are per-repo installs.`);
|
|
4891
|
+
}
|
|
4892
|
+
function stillOnElsewhere(targets) {
|
|
4893
|
+
const id = targets[0].id;
|
|
4894
|
+
const switched = new Set(targets.map((p) => p.projectPath).filter((p) => !!p));
|
|
4895
|
+
const left = installedPlugins().filter((p) => p.agent === "claude-code" && p.id === id && p.projectPath && !switched.has(p.projectPath) && p.on !== false);
|
|
4896
|
+
return left.length ? `Still on in ${left.map((p) => `${basename11(p.projectPath)} (${p.scope})`).join(", ")}.` : "";
|
|
4897
|
+
}
|
|
4606
4898
|
function show(args) {
|
|
4607
4899
|
const name = args.positional[0];
|
|
4608
|
-
const dir = name ?
|
|
4900
|
+
const dir = name ? join17(libraryDir(), name) : "";
|
|
4609
4901
|
if (!name || !isSkillDir(dir)) {
|
|
4610
4902
|
throw new Error(name ? `${name} is not in the library` : "Usage: skilllib show <skill-name>");
|
|
4611
4903
|
}
|
|
@@ -4618,7 +4910,7 @@ function show(args) {
|
|
|
4618
4910
|
latest: latestVersion(name)?.version ?? null,
|
|
4619
4911
|
origin: libraryOrigins()[name] ?? "",
|
|
4620
4912
|
projects: projectsUsing(name).map((p) => basename11(p)),
|
|
4621
|
-
skillMd: readFileSync7(
|
|
4913
|
+
skillMd: readFileSync7(join17(dir, "SKILL.md"), "utf-8")
|
|
4622
4914
|
});
|
|
4623
4915
|
}
|
|
4624
4916
|
info(`${name} ${dim(tildify(dir))}`);
|
|
@@ -4793,7 +5085,7 @@ function tidy(args) {
|
|
|
4793
5085
|
}
|
|
4794
5086
|
if (args.dryRun) info(dim("\nNothing changed (--dry-run)."));
|
|
4795
5087
|
else if (changed) info(dim(`
|
|
4796
|
-
Replaced copies are in ${tildify(
|
|
5088
|
+
Replaced copies are in ${tildify(join17(skilllibHome(), "tidy-backup"))}; \`skilllib restore\` puts them back.`));
|
|
4797
5089
|
if (held) {
|
|
4798
5090
|
process.exitCode = 1;
|
|
4799
5091
|
info(dim(`${held} change${held === 1 ? "" : "s"} held back because git would see them. Re-run with --allow-git to make them.`));
|
|
@@ -4914,7 +5206,7 @@ async function install(args) {
|
|
|
4914
5206
|
}
|
|
4915
5207
|
}
|
|
4916
5208
|
} finally {
|
|
4917
|
-
if (resolved.tmp)
|
|
5209
|
+
if (resolved.tmp) rmSync8(resolved.tmp, { recursive: true, force: true });
|
|
4918
5210
|
}
|
|
4919
5211
|
}
|
|
4920
5212
|
if (args.json) json("install", results);
|
|
@@ -5036,6 +5328,8 @@ export {
|
|
|
5036
5328
|
doctor,
|
|
5037
5329
|
apply,
|
|
5038
5330
|
usage,
|
|
5331
|
+
check,
|
|
5332
|
+
pluginCommand,
|
|
5039
5333
|
show,
|
|
5040
5334
|
harnessesCommand,
|
|
5041
5335
|
link,
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
add,
|
|
5
5
|
agentSkill,
|
|
6
6
|
apply,
|
|
7
|
+
check,
|
|
7
8
|
doctor,
|
|
8
9
|
error,
|
|
9
10
|
folders,
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
outdated,
|
|
20
21
|
overview,
|
|
21
22
|
parseArgs,
|
|
23
|
+
pluginCommand,
|
|
22
24
|
projects,
|
|
23
25
|
refreshAgentSkill,
|
|
24
26
|
remove,
|
|
@@ -33,7 +35,7 @@ import {
|
|
|
33
35
|
unhide,
|
|
34
36
|
update,
|
|
35
37
|
usage
|
|
36
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-XMXC2XM2.js";
|
|
37
39
|
|
|
38
40
|
// src/index.ts
|
|
39
41
|
import { readFileSync } from "fs";
|
|
@@ -59,8 +61,8 @@ Set up:
|
|
|
59
61
|
In a project (a git repo or a folder with skilllib.json; never your home folder):
|
|
60
62
|
skilllib status This project's skills, which agents load them, and issues
|
|
61
63
|
skilllib add <name>... Install library skills where your harnesses look
|
|
62
|
-
(
|
|
63
|
-
|
|
64
|
+
(your personal pins, kept out of git; --shared writes
|
|
65
|
+
skilllib.json to commit with the team)
|
|
64
66
|
skilllib remove <name>... Remove skills from this project
|
|
65
67
|
skilllib link [--all] Make every skill here usable by all your agents (adds links)
|
|
66
68
|
skilllib tidy [--all|--global] One real copy per skill, plus only the links your agents need
|
|
@@ -76,6 +78,8 @@ Library:
|
|
|
76
78
|
skilllib new <name> [desc] Create a skill in the library
|
|
77
79
|
skilllib projects Your projects and their skills
|
|
78
80
|
skilllib usage [--days N] Which skills agents used (from Claude Code transcripts)
|
|
81
|
+
skilllib check [--days N] What global skills and this repo's cost, and whether each was used or is current
|
|
82
|
+
skilllib plugin off|on <id> Turn a Claude Code plugin off or on (it stays installed)
|
|
79
83
|
|
|
80
84
|
Health:
|
|
81
85
|
skilllib doctor [--fix] Broken links, duplicates, out-of-date projects
|
|
@@ -88,7 +92,7 @@ Agents:
|
|
|
88
92
|
Teach your agents to use skilllib, so you can ask them
|
|
89
93
|
"which skills can you use here?" in any repo
|
|
90
94
|
--json Machine-readable output (status list show projects outdated
|
|
91
|
-
usage doctor apply add remove sync update import global move agent-skill install search)
|
|
95
|
+
usage doctor apply add remove sync update import global move agent-skill install search check plugin)
|
|
92
96
|
|
|
93
97
|
Options: --force (overwrite local edits) --allow-tracked (link into git-tracked skill folders) --days N
|
|
94
98
|
`.trim();
|
|
@@ -125,13 +129,15 @@ var FLAGS_OF = {
|
|
|
125
129
|
doctor: ["fix", "days"],
|
|
126
130
|
apply: ["days"],
|
|
127
131
|
usage: ["days"],
|
|
132
|
+
check: ["days"],
|
|
133
|
+
plugin: [],
|
|
128
134
|
install: ["force", "global", "all", "allow-tracked"],
|
|
129
135
|
search: [],
|
|
130
136
|
"agent-skill": [],
|
|
131
137
|
version: [],
|
|
132
138
|
help: []
|
|
133
139
|
};
|
|
134
|
-
var NO_ARGS = /* @__PURE__ */ new Set(["", "ui", "status", "outdated", "list", "projects", "link", "doctor", "usage", "version"]);
|
|
140
|
+
var NO_ARGS = /* @__PURE__ */ new Set(["", "ui", "status", "outdated", "list", "projects", "link", "doctor", "usage", "check", "version"]);
|
|
135
141
|
async function main() {
|
|
136
142
|
const argv = process.argv.slice(2);
|
|
137
143
|
setJsonMode(argv.includes("--json"));
|
|
@@ -148,10 +154,10 @@ async function main() {
|
|
|
148
154
|
if (!["--version", "-v", "version", "help", "--help", "-h"].includes(command ?? "")) refreshAgentSkill();
|
|
149
155
|
switch (command) {
|
|
150
156
|
case void 0:
|
|
151
|
-
return process.stdin.isTTY && process.stdout.isTTY ? (await import("./tui-
|
|
157
|
+
return process.stdin.isTTY && process.stdout.isTTY ? (await import("./tui-N6NRJYLZ.js")).tui() : overview(args);
|
|
152
158
|
case "ui":
|
|
153
159
|
if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("`skilllib ui` needs an interactive terminal. Agents and scripts: use `skilllib status --json`.");
|
|
154
|
-
return (await import("./tui-
|
|
160
|
+
return (await import("./tui-N6NRJYLZ.js")).tui();
|
|
155
161
|
case "status":
|
|
156
162
|
return status(args);
|
|
157
163
|
case "add":
|
|
@@ -196,6 +202,10 @@ async function main() {
|
|
|
196
202
|
return apply(args);
|
|
197
203
|
case "usage":
|
|
198
204
|
return usage(args);
|
|
205
|
+
case "check":
|
|
206
|
+
return check(args);
|
|
207
|
+
case "plugin":
|
|
208
|
+
return pluginCommand(args);
|
|
199
209
|
case "agent-skill":
|
|
200
210
|
return agentSkill(args);
|
|
201
211
|
case "install":
|
package/dist/json.d.cts
CHANGED
|
@@ -896,6 +896,43 @@ type InstallJson = {
|
|
|
896
896
|
/** Global folders written, when `--global`. */
|
|
897
897
|
global?: string[];
|
|
898
898
|
}[];
|
|
899
|
+
/** `skilllib check --json`. Computed while the command runs. Nothing is stored. */
|
|
900
|
+
type CheckJson = {
|
|
901
|
+
days: number;
|
|
902
|
+
repo: string | null;
|
|
903
|
+
/** Rough tokens of names and descriptions. Global skills load in every repo; repo skills add to that here. */
|
|
904
|
+
everyPrompt: {
|
|
905
|
+
global: number;
|
|
906
|
+
repo: number;
|
|
907
|
+
};
|
|
908
|
+
/** How to read uses and everyPrompt. */
|
|
909
|
+
note: string;
|
|
910
|
+
/** The short report. The skill list is there if you need a name. */
|
|
911
|
+
brief: string[];
|
|
912
|
+
skills: {
|
|
913
|
+
name: string;
|
|
914
|
+
where: "global" | "repo";
|
|
915
|
+
description: string;
|
|
916
|
+
everyPrompt: number;
|
|
917
|
+
uses: number;
|
|
918
|
+
lastUsed?: string;
|
|
919
|
+
projects?: string[];
|
|
920
|
+
fresh: string;
|
|
921
|
+
lever: "yours" | "plugin" | "built-in" | "claude.ai" | "system" | "skilllib" | "repo";
|
|
922
|
+
via?: string;
|
|
923
|
+
path: string;
|
|
924
|
+
alsoInRepo?: boolean;
|
|
925
|
+
keptGlobal?: boolean;
|
|
926
|
+
}[];
|
|
927
|
+
};
|
|
928
|
+
/** `skilllib plugin off|on <id> --json`. One Claude Code install: the copy that loads in every repo, or the only per-repo install. */
|
|
929
|
+
type PluginJson = {
|
|
930
|
+
id: string;
|
|
931
|
+
/** The state just set. */
|
|
932
|
+
on: boolean;
|
|
933
|
+
ok: boolean;
|
|
934
|
+
message: string;
|
|
935
|
+
};
|
|
899
936
|
/** `skilllib agent-skill [install|remove] --json` */
|
|
900
937
|
type AgentSkillJson = {
|
|
901
938
|
state: AgentSkillState;
|
|
@@ -905,4 +942,4 @@ type AgentSkillJson = {
|
|
|
905
942
|
removed?: string[];
|
|
906
943
|
};
|
|
907
944
|
|
|
908
|
-
export type { AgentSkillJson, AgentSkillState, AllChangesJson, ApplyJson, ChangeJson, ChangesJson, DoctorJson, ErrorJson, GitState, GlobalMoveJson, HarnessId, ImportJson, InstallJson, ListJson, OutdatedJson, ProjectSource, ProjectsJson, SearchJson, ShowJson, SkillState, SourceKind, StatusGlobalGroup, StatusIssue, StatusJson, StatusSkillGroup, UsageJson };
|
|
945
|
+
export type { AgentSkillJson, AgentSkillState, AllChangesJson, ApplyJson, ChangeJson, ChangesJson, CheckJson, DoctorJson, ErrorJson, GitState, GlobalMoveJson, HarnessId, ImportJson, InstallJson, ListJson, OutdatedJson, PluginJson, ProjectSource, ProjectsJson, SearchJson, ShowJson, SkillState, SourceKind, StatusGlobalGroup, StatusIssue, StatusJson, StatusSkillGroup, UsageJson };
|
package/dist/json.d.ts
CHANGED
|
@@ -896,6 +896,43 @@ type InstallJson = {
|
|
|
896
896
|
/** Global folders written, when `--global`. */
|
|
897
897
|
global?: string[];
|
|
898
898
|
}[];
|
|
899
|
+
/** `skilllib check --json`. Computed while the command runs. Nothing is stored. */
|
|
900
|
+
type CheckJson = {
|
|
901
|
+
days: number;
|
|
902
|
+
repo: string | null;
|
|
903
|
+
/** Rough tokens of names and descriptions. Global skills load in every repo; repo skills add to that here. */
|
|
904
|
+
everyPrompt: {
|
|
905
|
+
global: number;
|
|
906
|
+
repo: number;
|
|
907
|
+
};
|
|
908
|
+
/** How to read uses and everyPrompt. */
|
|
909
|
+
note: string;
|
|
910
|
+
/** The short report. The skill list is there if you need a name. */
|
|
911
|
+
brief: string[];
|
|
912
|
+
skills: {
|
|
913
|
+
name: string;
|
|
914
|
+
where: "global" | "repo";
|
|
915
|
+
description: string;
|
|
916
|
+
everyPrompt: number;
|
|
917
|
+
uses: number;
|
|
918
|
+
lastUsed?: string;
|
|
919
|
+
projects?: string[];
|
|
920
|
+
fresh: string;
|
|
921
|
+
lever: "yours" | "plugin" | "built-in" | "claude.ai" | "system" | "skilllib" | "repo";
|
|
922
|
+
via?: string;
|
|
923
|
+
path: string;
|
|
924
|
+
alsoInRepo?: boolean;
|
|
925
|
+
keptGlobal?: boolean;
|
|
926
|
+
}[];
|
|
927
|
+
};
|
|
928
|
+
/** `skilllib plugin off|on <id> --json`. One Claude Code install: the copy that loads in every repo, or the only per-repo install. */
|
|
929
|
+
type PluginJson = {
|
|
930
|
+
id: string;
|
|
931
|
+
/** The state just set. */
|
|
932
|
+
on: boolean;
|
|
933
|
+
ok: boolean;
|
|
934
|
+
message: string;
|
|
935
|
+
};
|
|
899
936
|
/** `skilllib agent-skill [install|remove] --json` */
|
|
900
937
|
type AgentSkillJson = {
|
|
901
938
|
state: AgentSkillState;
|
|
@@ -905,4 +942,4 @@ type AgentSkillJson = {
|
|
|
905
942
|
removed?: string[];
|
|
906
943
|
};
|
|
907
944
|
|
|
908
|
-
export type { AgentSkillJson, AgentSkillState, AllChangesJson, ApplyJson, ChangeJson, ChangesJson, DoctorJson, ErrorJson, GitState, GlobalMoveJson, HarnessId, ImportJson, InstallJson, ListJson, OutdatedJson, ProjectSource, ProjectsJson, SearchJson, ShowJson, SkillState, SourceKind, StatusGlobalGroup, StatusIssue, StatusJson, StatusSkillGroup, UsageJson };
|
|
945
|
+
export type { AgentSkillJson, AgentSkillState, AllChangesJson, ApplyJson, ChangeJson, ChangesJson, CheckJson, DoctorJson, ErrorJson, GitState, GlobalMoveJson, HarnessId, ImportJson, InstallJson, ListJson, OutdatedJson, PluginJson, ProjectSource, ProjectsJson, SearchJson, ShowJson, SkillState, SourceKind, StatusGlobalGroup, StatusIssue, StatusJson, StatusSkillGroup, UsageJson };
|
|
@@ -88,7 +88,7 @@ import {
|
|
|
88
88
|
versionHistory,
|
|
89
89
|
visibleProjects,
|
|
90
90
|
withoutForceHint
|
|
91
|
-
} from "./chunk-
|
|
91
|
+
} from "./chunk-XMXC2XM2.js";
|
|
92
92
|
|
|
93
93
|
// src/tui/index.tsx
|
|
94
94
|
import { render } from "ink";
|
|
@@ -303,17 +303,6 @@ function copyText(text) {
|
|
|
303
303
|
return { copied: false, savedTo };
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
-
// src/search.ts
|
|
307
|
-
function matchScore(text, query) {
|
|
308
|
-
if (!query) return 0;
|
|
309
|
-
const t = text.toLowerCase();
|
|
310
|
-
const q = query.toLowerCase();
|
|
311
|
-
if (t.startsWith(q)) return 0;
|
|
312
|
-
if (t.includes(q)) return 1;
|
|
313
|
-
const initials = t.split(/[-_\s./]+/).filter(Boolean).map((w) => w[0]).join("");
|
|
314
|
-
return q.length >= 2 && initials.startsWith(q) ? 2 : null;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
306
|
// src/tui/world.ts
|
|
318
307
|
var failed = (message) => ({ message, failed: true });
|
|
319
308
|
var isFailure = (r) => typeof r !== "string" && "failed" in r;
|
|
@@ -624,7 +613,12 @@ function groupLabel(g, names) {
|
|
|
624
613
|
return `${top} + ${names.length - 1} more \xB7 ${g.label}`;
|
|
625
614
|
}
|
|
626
615
|
function matches(name, query) {
|
|
627
|
-
|
|
616
|
+
if (!query) return true;
|
|
617
|
+
const text = name.toLowerCase();
|
|
618
|
+
const q = query.toLowerCase();
|
|
619
|
+
if (text.includes(q)) return true;
|
|
620
|
+
const initials = text.split(/[-_\s./]+/).filter(Boolean).map((w) => w[0]).join("");
|
|
621
|
+
return q.length >= 2 && initials.startsWith(q);
|
|
628
622
|
}
|
|
629
623
|
function addRows(w, target, q) {
|
|
630
624
|
const taken = new Set(target ? project(w, target).skills.map((s) => s.name) : []);
|
package/package.json
CHANGED
package/skills/skilllib/SKILL.md
CHANGED
|
@@ -6,7 +6,7 @@ allowed-tools: Bash(skilllib:*)
|
|
|
6
6
|
|
|
7
7
|
# skilllib
|
|
8
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.
|
|
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. Adding a skill pins it in the user's personal pins (`~/.skilllib/pins`, kept out of git: `personal: true`). `skilllib.json` is only for sharing a skill with the team, and only when they ask: `--shared` writes it, and they commit it.
|
|
10
10
|
|
|
11
11
|
Always pass `--json` (one line of JSON on stdout; messages go to stderr). A failure exits 1 with `{"error": "..."}` as that line. `link`, `tidy`, `global` (its listing, `keep` and `unkeep`), `restore`, `new`, `folders`, `harnesses`, `scan`, `search` (without `--json`) and `unhide` print messages only (read the exit code). Unknown flags are errors. Never run bare `skilllib` or `skilllib ui`: that's an interactive app.
|
|
12
12
|
|
|
@@ -23,9 +23,11 @@ The **Now** section at the end has this repo's `status` and `list` output. Answe
|
|
|
23
23
|
|
|
24
24
|
Also: `usage --json` (Claude Code usage only, so "unused" is a hint; each skill lists the `projects` that used it), `projects --json` (every repo), `outdated --all --json`, `doctor --json` (every issue in every repo and globally, each with an `id`).
|
|
25
25
|
|
|
26
|
+
`check --json` reads the global skills and, in a repo, that repo's. It stores nothing. `brief` is the report: how many tokens the names and descriptions cost on every prompt, whether that is heavy, and the cuts that can actually be made. A Claude Code plugin it names turns off with `plugin off <id> --json`, and `plugin on <id>` puts it back. Run that only after the user agrees: the switch is global, and a skill in the plugin that was used goes too. Cursor plugins turn off in Cursor. claude.ai skills turn off on claude.ai. Built-ins ship with Cursor, Codex, and Grok, and skilllib can't unload them. Your own unused globals can move into the repos that need them. `uses` is Claude Code only, so 0 means it wasn't seen. Read `brief` to the user. The `skills` list is there if they ask about one.
|
|
27
|
+
|
|
26
28
|
## Changing (ask first unless the user asked for this exact change)
|
|
27
29
|
|
|
28
|
-
`add <name>...` (pins
|
|
30
|
+
`plugin off|on <id>` (a Claude Code plugin, `name@marketplace` from `check`; a name alone works when only one plugin has it; it stays installed. The copy that loads in every repo. A per-repo install is left on, and the message says where) · `add <name>...` (personal pins; `--shared` writes `skilllib.json` for the team, and only when they asked to share it; `--personal` moves a shared pin back. Either flag moves an existing pin without changing its version. A pin stays where it already is) · `remove <name>...` (skills skilllib manages, `source: library`) · `update [name...]` · `sync` (installs what's pinned, personal pins and `skilllib.json`; takes no names). Pass several names in one call. Their result is `[{name, action, from, to, dirs, personal, moved, reason, blocked, uncommitted, inTheWay, backedUp}]` (`personal: true` when the pin is in your personal pins; `moved` when `--personal`/`--shared` moved it), which is the whole result: there's no need to check files afterwards. `import <dir>...` (into the library) prints `[{path, name, status}]` and exits 1 if a folder was skipped (`exists`, `no SKILL.md`). `doctor --fix` prints doctor's issues, each fix with `fixed: {ok, message}`.
|
|
29
31
|
|
|
30
32
|
- `skipped` + `reason`: nothing changed for that skill, and the command exits 1; tell the user. `--force` overwrites local edits (`backedUp` says where they went; `skilllib restore <name>` brings them back), but only use it with their OK.
|
|
31
33
|
- `removed` + `reason`: removed from skilllib.json, but something was left as it is (its copy behind a folder that links out of the repo, a file of the user's, or a link in such a folder). Tell the user what the reason says is still there.
|
|
@@ -33,11 +35,11 @@ Also: `usage --json` (Claude Code usage only, so "unused" is a hint; each skill
|
|
|
33
35
|
- `inTheWay`: something else is where a link goes (a file, or another copy), so that agent may not run this copy there, and the command exits 1. Tell the user what's there; once they move it, `skilllib link` adds the link. (A skills folder that's a file or links out of the repo makes the skill `skipped` instead, with the reason.)
|
|
34
36
|
- `blocked`: git tracks that folder. Re-run with `--allow-tracked` (for that command only) only with their OK.
|
|
35
37
|
- `uncommitted`: git tracks that folder but not the skill's real copy, so a link there would break for teammates; `--allow-tracked` doesn't change that. Once the copy is committed, `link --allow-tracked` adds it.
|
|
36
|
-
- Changes apply from the agent's next session.
|
|
38
|
+
- Changes apply from the agent's next session. A `personal` change has nothing to commit. Without `personal` (`--shared`, or a pin that was already in `skilllib.json`), remind the user to commit `skilllib.json` and the folders in `dirs`.
|
|
37
39
|
|
|
38
40
|
**Prefer per-repo skills to global ones.** A global skill loads in every repo, for every agent. When `usage --json` shows one used in only a few repos, propose moving it there:
|
|
39
41
|
|
|
40
|
-
- `global move <name>... --to <repo>[,<repo>...]` (a project name from `projects`, or the path of a repo's top folder): imports it into the library, adds it to each repo, and only then stops loading it globally (restorable with `skilllib restore`). Prints `[{skill, moved, reason, left, projects: [{project, path, change}]}]` (`left`: links in a repo or its packages that now point at nothing, though a copy in the repo still serves them; tell the user to remove them); `moved: false` means it still loads globally, and `reason` says why
|
|
42
|
+
- `global move <name>... --to <repo>[,<repo>...]` (a project name from `projects`, or the path of a repo's top folder): imports it into the library, adds it to each repo, and only then stops loading it globally (restorable with `skilllib restore`). Prints `[{skill, moved, reason, left, projects: [{project, path, change}]}]` (`left`: links in a repo or its packages that now point at nothing, though a copy in the repo still serves them; tell the user to remove them); `moved: false` means it still loads globally, and `reason` says why.
|
|
41
43
|
- `global keep <name>...`: the user wants it everywhere; skilllib stops flagging it.
|
|
42
44
|
|
|
43
45
|
**Fixes and decisions.** `apply <id> [choice]` runs one problem's `fix`, or the choice you name: its label (safest), or its number from 1. With several choices it needs one. Prints `{id, applied, ok, message}`. Ids come from `issues` in `status --json` (Now, below) or from `doctor --json` (every repo; pass `apply` the same `--days` you gave `doctor`). A few issues span repos and offer different choices in each; then `apply` takes only the label.
|