skilllib 1.10.0 → 1.11.1
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 +53 -43
- package/dist/{chunk-Z2F5W3DY.js → chunk-LO4FMXDE.js} +132 -33
- package/dist/index.js +10 -4
- package/dist/json.d.cts +25 -3
- package/dist/json.d.ts +25 -3
- package/dist/{tui-7CMGG6HX.js → tui-3LDZ5GOZ.js} +70 -30
- package/package.json +1 -1
- package/skills/skilllib/SKILL.md +12 -5
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# ◆ skilllib
|
|
4
4
|
|
|
5
|
-
**Manage your agent skills like dependencies.**<br>
|
|
5
|
+
**Manage your agent skills like dependencies, or let your agents do it.**<br>
|
|
6
6
|
One library for all your skills. Each repo gets only the ones it needs,<br>
|
|
7
7
|
and every agent you use can see them.
|
|
8
8
|
|
|
@@ -12,9 +12,11 @@ and every agent you use can see them.
|
|
|
12
12
|

|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
|
|
15
|
+
npm install -g skilllib && skilllib agent-skill install
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
Then ask your agent: *"Which skills can you use here, and which of my global skills should only load in this repo?"*
|
|
19
|
+
|
|
18
20
|
<img src="docs/design/repo.svg" alt="skilllib: Places on the left, a repo's skills with their issues and uses, and the selected skill's fixes and actions below" width="100%">
|
|
19
21
|
|
|
20
22
|
</div>
|
|
@@ -43,18 +45,18 @@ Most agents can't turn off a global skill for just one repo. Claude Code can, bu
|
|
|
43
45
|
|
|
44
46
|
## How skilllib fixes it
|
|
45
47
|
|
|
48
|
+
- **🤖 Your agents run it.** One small skill teaches Claude Code, Cursor, Codex, Zed and Grok to use the skilllib CLI. Ask them what's loaded here, what to add, what's out of date, or to move a global skill into the repos that use it. Every command has `--json` output shaped for agents, and they ask before changing anything.
|
|
46
49
|
- **📚 Your skills: one versioned library.** Every skill has a single master copy in `~/.skilllib/library`, and each change becomes a new version.
|
|
47
50
|
- **📦 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
51
|
- **🤝 One copy, every agent.** skilllib puts one real copy in the repo and links it into whichever folders your other agents read.
|
|
49
|
-
- **🧹 Health: one list of what to fix.** Global skills you haven't decided about, skills loaded twice, duplicate copies, updates, broken links, each with its fix. For a global skill: move it to the repos that need it, keep it global, or delete it. Usage numbers and descriptions help you decide.
|
|
52
|
+
- **🧹 Health: one list of what to fix.** Global skills you haven't decided about, skills loaded twice, duplicate copies, updates, broken links, each with its fix. For a global skill: move it to the repos that need it, keep it global, or delete it. Usage numbers and descriptions help you, or your agent, decide.
|
|
50
53
|
- **✓ Global on purpose.** Some skills belong everywhere. Mark them once and skilllib stops warning about them.
|
|
51
54
|
- **🔎 See everything in a repo:**
|
|
52
55
|
- where each skill comes from (the repo, your library, global, a plugin, claude.ai…)
|
|
53
56
|
- which agents load it
|
|
54
57
|
- whether it's committed to git or gitignored
|
|
55
58
|
- how often it was used
|
|
56
|
-
-
|
|
57
|
-
- **✦ Ask an agent.** Copy a ready-made prompt with all of that data, and let Claude Code, Cursor, Codex or Grok tell you what to keep, move or delete.
|
|
59
|
+
- **🖥 An app when you want to look yourself.** `skilllib` opens a terminal app with the same data and fixes, and can copy a review prompt for any agent.
|
|
58
60
|
- **🛟 Nothing is lost.** Every removal goes to a backup you can restore with one key.
|
|
59
61
|
|
|
60
62
|
Everything runs locally: no server, no account, no network calls.
|
|
@@ -63,17 +65,27 @@ Everything runs locally: no server, no account, no network calls.
|
|
|
63
65
|
|
|
64
66
|
## Quick start
|
|
65
67
|
|
|
68
|
+
It needs **Node.js 22+** and works on macOS and Linux. Windows support is newer: it uses directory junctions, so no admin rights are needed.
|
|
69
|
+
|
|
70
|
+
### With your agents
|
|
71
|
+
|
|
66
72
|
```bash
|
|
67
|
-
npm install -g skilllib
|
|
68
|
-
skilllib
|
|
73
|
+
npm install -g skilllib
|
|
74
|
+
skilllib agent-skill install
|
|
69
75
|
```
|
|
70
76
|
|
|
71
|
-
|
|
77
|
+
This installs the `skilllib` skill where your agents look, in every repo (see [Use it from your agents](#use-it-from-your-agents)). Start a new agent session in any repo and ask away.
|
|
78
|
+
|
|
79
|
+
### In the app
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
skilllib # or: npx skilllib
|
|
83
|
+
```
|
|
72
84
|
|
|
73
85
|
On first run, skilllib asks two things:
|
|
74
86
|
|
|
75
87
|
1. **Which agents you use:** Claude Code, Cursor, Codex, Zed, Grok. The ones installed on your machine are pre-selected.
|
|
76
|
-
2. **Where your projects live**, e.g. `~/Projects`. It finds every git repo in there, and rescans each time it opens.
|
|
88
|
+
2. **Where your projects live**, e.g. `~/Projects`. It finds every git repo in there, and rescans each time it opens. Agent worktrees (`<repo>/.claude/worktrees/*`) aren't counted as projects.
|
|
77
89
|
|
|
78
90
|
Then a good first session looks like this:
|
|
79
91
|
|
|
@@ -83,7 +95,26 @@ Then a good first session looks like this:
|
|
|
83
95
|
|
|
84
96
|
---
|
|
85
97
|
|
|
86
|
-
##
|
|
98
|
+
## Use it from your agents
|
|
99
|
+
|
|
100
|
+
`skilllib agent-skill install` puts a small `skilllib` skill in `~/.claude/skills` (Claude Code, Cursor and Grok read it there), with a link in `~/.agents/skills` (Codex and Zed). This is the one skill that should be global, because it's about every repo. Upgrading skilllib keeps it up to date, and `skilllib agent-skill remove` takes it out. Agents use it from their next session.
|
|
101
|
+
|
|
102
|
+
Then ask in plain words. A few examples, with what the agent runs:
|
|
103
|
+
|
|
104
|
+
| Ask | The agent runs |
|
|
105
|
+
|---|---|
|
|
106
|
+
| *Which skills can you use here, and where does each come from?* | `status --json`, which Claude Code gets as the skill loads, so it answers in one step |
|
|
107
|
+
| *Which of my skills should this repo use?* | `list --json`, then reads the README and manifests, and gives a reason for each pick |
|
|
108
|
+
| *Which skills do I use most? Which ones never?* | `usage --json` (Claude Code usage only, so "never" is a hint) |
|
|
109
|
+
| *Move my global skills into the repos that actually use them.* | `usage --json` to see where each is used, then `global move <name> --to <repo>`, or `global keep <name>` for the ones you want everywhere |
|
|
110
|
+
| *Is anything broken or out of date? Fix it.* | `status --json` or `doctor --json`, then `doctor --fix` for the safe repairs and `apply <id> <choice>` for each decision you approve |
|
|
111
|
+
| *Add the skill from github.com/acme/skills to my library.* | Clones the repo into a temporary folder, then `import <dir>` and `add <name>`. skilllib itself downloads nothing |
|
|
112
|
+
|
|
113
|
+
The agent asks before it changes anything, unless you asked for that exact change. Anything skilllib removes can be restored: see [Backups](#backups).
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## The app
|
|
87
118
|
|
|
88
119
|
### The screen
|
|
89
120
|
|
|
@@ -143,7 +174,7 @@ What skilllib flags:
|
|
|
143
174
|
|
|
144
175
|
### Adding and removing skills
|
|
145
176
|
|
|
146
|
-
- **+ Add a skill…** searches your library. Its last rows create a new skill (it opens in `$EDITOR`, and goes into the repo when you close it) or copy a prompt for an agent to write it.
|
|
177
|
+
- **+ Add a skill…** searches your library. Tick several with `space` and add them in one go; a plugin's skills (e.g. ponytail and its `ponytail-*` commands) sit under one row that ticks or adds them all. Its last rows create a new skill (it opens in `$EDITOR`, and goes into the repo when you close it) or copy a prompt for an agent to write it.
|
|
147
178
|
- **Adding** copies the newest version into the fewest folders your agents read, and records it in `skilllib.json`. Agents pick it up in their next session. The layout matches `npx skills`' symlink option: the real copy lives in `.agents/skills`, which nearly every agent reads, and Claude Code gets a link. Grok reads `.agents/skills` and, by default, `.claude/skills`, so it never needs its own copy.
|
|
148
179
|
- Claude Code, with or without Cursor or Grok: `.claude/skills/<name>`.
|
|
149
180
|
- Cursor, Codex, Zed or Grok without Claude Code: `.agents/skills/<name>`.
|
|
@@ -175,7 +206,7 @@ Edit a skill once in **Your skills** (Enter → *Edit SKILL.md*). Every repo tha
|
|
|
175
206
|
|
|
176
207
|
**Global** lists everything that loads in every repo. Each of your global skills you haven't decided about is flagged **Not reviewed**, with three choices:
|
|
177
208
|
|
|
178
|
-
- **Move it to the repos that need it…**: tick the repos (the ones where Claude Code used it are pre-ticked). It goes into your library and those repos, and stops loading globally. If a repo commits a folder one of your agents needs a link in (often `.claude/skills`), skilllib asks before adding it there.
|
|
209
|
+
- **Move it to the repos that need it…**: tick the repos (the ones where Claude Code used it are pre-ticked). It goes into your library and those repos, and stops loading globally. If a repo commits a folder one of your agents needs a link in (often `.claude/skills`), skilllib asks before adding it there. From the command line: `skilllib global move <name>... --to <repo>...`.
|
|
179
210
|
- **Keep it global on purpose**: skilllib stops warning about it (also `skilllib global keep <name>`). The mark only records your decision; to undo it, pick **Stop marking it as global on purpose**.
|
|
180
211
|
- **Delete it**: it goes to Settings › Backups.
|
|
181
212
|
|
|
@@ -224,25 +255,6 @@ Paste it into Claude Code, Cursor, Codex or Grok. You get back **keep global / m
|
|
|
224
255
|
|
|
225
256
|
It's in every skill's actions, in a repo's **⋯** menu (all its skills), and at the top of Global (all your global skills).
|
|
226
257
|
|
|
227
|
-
### Ask your agents about skills
|
|
228
|
-
|
|
229
|
-
Run this once:
|
|
230
|
-
|
|
231
|
-
```bash
|
|
232
|
-
skilllib agent-skill install
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
This installs a small `skilllib` skill that tells your agents how to use skilllib. It goes where your agents look, in every repo: `~/.claude/skills` for Claude Code and Cursor, with a link in `~/.agents/skills` for Codex and Zed. This is the one skill that should be global, because it's about every repo. Health and Settings offer the same install, and upgrading skilllib keeps the skill up to date.
|
|
236
|
-
|
|
237
|
-
Then, in any repo, you can ask:
|
|
238
|
-
|
|
239
|
-
- *Which skills can you use in this repo?*
|
|
240
|
-
- *Where does each one come from?*
|
|
241
|
-
- *Which skills from my library would you add to this project?*
|
|
242
|
-
- *Is anything out of date? Update it.*
|
|
243
|
-
|
|
244
|
-
The agent runs `skilllib status --json`, `skilllib list --json` and similar commands, reads the repo, and answers. It asks before it adds or removes anything. `skilllib agent-skill remove` uninstalls it.
|
|
245
|
-
|
|
246
258
|
### Duplicates
|
|
247
259
|
|
|
248
260
|
The same skill often reaches your agents more than once: a copy in `.claude/skills` *and* in `.agents/skills`, a global copy *and* a repo copy, a plugin *and* your skill. skilllib keeps **one real copy per skill, plus only the links your agents need**. **Health** lists each case on the skill it's about, with its fix. From the command line, `skilllib doctor` lists them and `skilllib tidy` fixes the folder ones.
|
|
@@ -351,7 +363,7 @@ A skill is a folder with a `SKILL.md`. These are all the places your agents load
|
|
|
351
363
|
|
|
352
364
|
## Command line
|
|
353
365
|
|
|
354
|
-
Everything the app does also works as a command, for scripts and CI:
|
|
366
|
+
Everything the app does also works as a command, for your agents, scripts and CI:
|
|
355
367
|
|
|
356
368
|
```
|
|
357
369
|
skilllib open the app (prints `status` when piped)
|
|
@@ -370,10 +382,12 @@ skilllib tidy [--all|--global] one real copy per skill, plus only the links you
|
|
|
370
382
|
skilllib list | show <name> your library
|
|
371
383
|
skilllib import <dir>... add skill folders to your library (--global: all your global skills)
|
|
372
384
|
skilllib global [keep|unkeep] your global skills; mark the ones you keep global on purpose
|
|
385
|
+
skilllib global move <name> --to <repo> load a global skill only in those repos
|
|
373
386
|
skilllib new <name> [desc] create a skill
|
|
374
387
|
|
|
375
388
|
skilllib usage [--days N] which skills Claude Code used
|
|
376
389
|
skilllib doctor [--fix] find (and fix) problems
|
|
390
|
+
skilllib apply <id> [choice] apply one problem's fix, or the choice you pick (ids from status or doctor --json)
|
|
377
391
|
skilllib restore [name] put back something skilllib moved out
|
|
378
392
|
skilllib agent-skill [install|remove] teach your agents to use skilllib
|
|
379
393
|
skilllib --version
|
|
@@ -385,13 +399,13 @@ Flags: `--force` (overwrite local edits, backed up to `~/.skilllib/edit-backup`)
|
|
|
385
399
|
|
|
386
400
|
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, an unknown `tidy` name or `--keep` folder. What it did do is still done, and listed.
|
|
387
401
|
|
|
388
|
-
**`--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`, `add`, `remove`, `sync`, `update`, `import` and `agent-skill`. The output is shaped to keep agents fast and cheap:
|
|
402
|
+
**`--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 `agent-skill`. The output is shaped to keep agents fast and cheap:
|
|
389
403
|
|
|
390
404
|
- **`status --json`** answers everything about the repo in one call:
|
|
391
405
|
- the repo's skills, with their source, version, state and git state
|
|
392
406
|
- skills in subfolders (`nested`, e.g. monorepo packages), with their folder
|
|
393
407
|
- the global skills, grouped by where they come from, with the plugins the repo's `.claude/settings.json` turns on or off
|
|
394
|
-
- every problem skilllib found here, with its fix
|
|
408
|
+
- every problem skilllib found here, with its fix or choices and an id for `skilllib apply`
|
|
395
409
|
- how often Claude Code used each skill here
|
|
396
410
|
- the repo's top-level files
|
|
397
411
|
|
|
@@ -440,7 +454,7 @@ Environment variables: `SKILLLIB_HOME` moves `~/.skilllib`, `$VISUAL` / `$EDITOR
|
|
|
440
454
|
- Turning off Cursor marketplace plugins: skilllib lists them and reports ones that duplicate your skills, but Cursor doesn't record which plugins are on in a file skilllib can read.
|
|
441
455
|
- Installing plugins from a marketplace: use `/plugin` in Claude Code. Plugins a repo's own `.claude/settings.json` turns on without installing them aren't listed in Plugins yet.
|
|
442
456
|
- Unloading a `npx skills` skill doesn't update that tool's lock file.
|
|
443
|
-
-
|
|
457
|
+
- Downloading skills: skilllib makes no network calls. To get a skill from GitHub, clone the repo and run `skilllib import <folder>`, or ask your agent to.
|
|
444
458
|
|
|
445
459
|
---
|
|
446
460
|
|
|
@@ -456,14 +470,10 @@ npm link # use your checkout as `skilllib`
|
|
|
456
470
|
|
|
457
471
|
How the TUI is designed, and why: [docs/DESIGN.md](docs/DESIGN.md).
|
|
458
472
|
|
|
459
|
-
**Releasing:**
|
|
460
|
-
1.
|
|
461
|
-
2.
|
|
462
|
-
|
|
463
|
-
git tag v1.10.0 && git push --tags
|
|
464
|
-
```
|
|
465
|
-
|
|
466
|
-
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.
|
|
473
|
+
**Releasing:** automatic, with [release-please](https://github.com/googleapis/release-please). Don't edit `version` by hand.
|
|
474
|
+
1. Write commits (and PR titles) as [Conventional Commits](https://www.conventionalcommits.org/): `fix:` → patch, `feat:` → minor, `feat!:` or a `BREAKING CHANGE:` footer → major. `docs:`, `ci:`, `chore:`, `test:` and `refactor:` don't release.
|
|
475
|
+
2. After each merge to `main`, release-please keeps one PR, **chore(main): release X.Y.Z**, up to date with the next version and its `CHANGELOG.md`.
|
|
476
|
+
3. Merge that PR when you want to release. It tags `vX.Y.Z` and creates the GitHub release; CI then tests, builds and publishes to npm through [trusted publishing](https://docs.npmjs.com/trusted-publishers) (no token; provenance is automatic).
|
|
467
477
|
|
|
468
478
|
The earlier registry-server version (Next.js, Postgres, S3) lives on the `archive/registry-server` branch.
|
|
469
479
|
|
|
@@ -342,7 +342,10 @@ function knownProjects() {
|
|
|
342
342
|
const path = projectsFile();
|
|
343
343
|
if (!existsSync4(path)) return [];
|
|
344
344
|
const list2 = JSON.parse(readFileSync3(path, "utf-8"));
|
|
345
|
-
return onePerFolder(list2.filter((p) => existsSync4(p) && isProjectCandidate(p)));
|
|
345
|
+
return onePerFolder(list2.filter((p) => existsSync4(p) && isProjectCandidate(p) && !isAgentWorktree(p)));
|
|
346
|
+
}
|
|
347
|
+
function isAgentWorktree(path) {
|
|
348
|
+
return /[\\/]\.claude[\\/]worktrees[\\/]/.test(path);
|
|
346
349
|
}
|
|
347
350
|
function onePerFolder(paths) {
|
|
348
351
|
const byReal = /* @__PURE__ */ new Map();
|
|
@@ -357,7 +360,7 @@ function isProjectCandidate(root) {
|
|
|
357
360
|
return realPath(root) !== realPath(userHome());
|
|
358
361
|
}
|
|
359
362
|
function rememberProjects(roots) {
|
|
360
|
-
const incoming = roots.map((r) => resolve3(r)).filter(isProjectCandidate);
|
|
363
|
+
const incoming = roots.map((r) => resolve3(r)).filter((r) => isProjectCandidate(r) && !isAgentWorktree(r));
|
|
361
364
|
const all = onePerFolder([...knownProjects(), ...incoming]).sort();
|
|
362
365
|
mkdirSync2(skilllibHome(), { recursive: true });
|
|
363
366
|
writeFileSync2(projectsFile(), JSON.stringify(all, null, 2) + "\n");
|
|
@@ -1262,7 +1265,7 @@ function refreshAgentSkill() {
|
|
|
1262
1265
|
|
|
1263
1266
|
// src/commands.ts
|
|
1264
1267
|
import { existsSync as existsSync13, readFileSync as readFileSync9 } from "fs";
|
|
1265
|
-
import { basename as
|
|
1268
|
+
import { basename as basename9, join as join14, resolve as resolve7, sep as sep5 } from "path";
|
|
1266
1269
|
|
|
1267
1270
|
// src/git.ts
|
|
1268
1271
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
@@ -1989,7 +1992,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
1989
1992
|
id: "review:global",
|
|
1990
1993
|
severity: "suggestion",
|
|
1991
1994
|
title: `${plural(unreviewed.length, "global skill")} of yours ${unreviewed.length === 1 ? "loads" : "load"} in every repo`,
|
|
1992
|
-
detail: `${unreviewed.join(", ")}. Move each to the repos that need it (skilllib
|
|
1995
|
+
detail: `${unreviewed.join(", ")}. Move each to the repos that need it (skilllib global move <name> --to <repo>...), or mark the ones you want everywhere as global on purpose (skilllib global keep <name>).`
|
|
1993
1996
|
});
|
|
1994
1997
|
}
|
|
1995
1998
|
const enabled = enabledHarnesses();
|
|
@@ -2015,7 +2018,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
|
|
|
2015
2018
|
id: `usable:global${onPurpose ? ":kept" : ""}`,
|
|
2016
2019
|
severity: "suggestion",
|
|
2017
2020
|
title: `${plural(skills.length, "global skill")}${onPurpose ? " you keep global" : ""} not usable by ${agentNames(missing)}`,
|
|
2018
|
-
detail: `${skills.map((m) => `${m.name} (only ${agentNames(m.harnesses)})`).join(", ")}. ${onPurpose ? "A link in each agent's global folder makes the one real copy reach every agent you use." : `Link ${skills.length === 1 ? "it" : "them"} for every agent, or move ${skills.length === 1 ? "it" : "them"} to the repos that need ${skills.length === 1 ? "it" : "them"} (skilllib
|
|
2021
|
+
detail: `${skills.map((m) => `${m.name} (only ${agentNames(m.harnesses)})`).join(", ")}. ${onPurpose ? "A link in each agent's global folder makes the one real copy reach every agent you use." : `Link ${skills.length === 1 ? "it" : "them"} for every agent, or move ${skills.length === 1 ? "it" : "them"} to the repos that need ${skills.length === 1 ? "it" : "them"} (skilllib global move), which works for every agent too.`}`,
|
|
2019
2022
|
...onPurpose ? { fix: link2 } : { choices: [link2] }
|
|
2020
2023
|
});
|
|
2021
2024
|
}
|
|
@@ -2377,6 +2380,9 @@ function topLevel(root) {
|
|
|
2377
2380
|
return [];
|
|
2378
2381
|
}
|
|
2379
2382
|
}
|
|
2383
|
+
function statusIssues(root, machine = machineSkills(enabledHarnesses())) {
|
|
2384
|
+
return findIssues(projectHere(root) === root ? [root] : [], machine, new Set(librarySkills().map((l) => l.name)));
|
|
2385
|
+
}
|
|
2380
2386
|
function usableHere(root, uses = /* @__PURE__ */ new Map()) {
|
|
2381
2387
|
const agents = enabledHarnesses();
|
|
2382
2388
|
const inRepo = projectHere(root) === root;
|
|
@@ -2412,11 +2418,12 @@ function usableHere(root, uses = /* @__PURE__ */ new Map()) {
|
|
|
2412
2418
|
})
|
|
2413
2419
|
);
|
|
2414
2420
|
const issues = /* @__PURE__ */ new Map();
|
|
2415
|
-
for (const i of
|
|
2421
|
+
for (const i of statusIssues(root, machine)) {
|
|
2416
2422
|
const choices = i.choices?.map((c) => c.label);
|
|
2417
2423
|
const key = `${i.detail}\0${i.fix?.label ?? ""}\0${choices?.join() ?? ""}`;
|
|
2418
|
-
const issue = issues.get(key) ?? { problems: [], detail: i.detail, fix: i.fix?.label, choices };
|
|
2424
|
+
const issue = issues.get(key) ?? { problems: [], ids: [], detail: i.detail, fix: i.fix?.label, choices };
|
|
2419
2425
|
issue.problems.push(i.title);
|
|
2426
|
+
issue.ids.push(i.id);
|
|
2420
2427
|
issues.set(key, issue);
|
|
2421
2428
|
}
|
|
2422
2429
|
const usesHere = Object.fromEntries([...uses].filter(([, n]) => n > 0));
|
|
@@ -2576,6 +2583,25 @@ function usesByProject(uses, projectOf) {
|
|
|
2576
2583
|
return byProject;
|
|
2577
2584
|
}
|
|
2578
2585
|
|
|
2586
|
+
// src/move.ts
|
|
2587
|
+
import { basename as basename8 } from "path";
|
|
2588
|
+
function moveGlobalSkills(skills, roots, { group, allowTracked = false, label = basename8 } = {}) {
|
|
2589
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2590
|
+
for (const m of skills) byName.set(m.name, [...byName.get(m.name) ?? [], m]);
|
|
2591
|
+
return [...byName].map(([name, copies]) => {
|
|
2592
|
+
if (new Set(copies.map((m) => treeHash(m.path))).size > 1)
|
|
2593
|
+
return { name, moved: false, reason: `your global copies of ${name} differ; nothing moved (keep one first: skilllib tidy --global)`, changes: [] };
|
|
2594
|
+
if (importSkill(copies[0].path).status === "exists")
|
|
2595
|
+
return { name, moved: false, reason: `your library has a different ${name}; nothing moved (update your library from this copy first)`, changes: [] };
|
|
2596
|
+
if (group) recordOrigin(name, `group: ${group}`);
|
|
2597
|
+
const changes = roots.map((root) => [root, addSkill(root, name, { allowTracked })]);
|
|
2598
|
+
const skipped = changes.filter(([, c]) => c.action === "skipped");
|
|
2599
|
+
if (skipped.length) return { name, moved: false, reason: `not added to ${skipped.map(([r, c]) => `${label(r)} (${c.reason})`).join(", ")}, so it still loads globally`, changes };
|
|
2600
|
+
const failed = copies.map((m) => unloadGlobal(m.path, m.links)).find((r) => !r.ok);
|
|
2601
|
+
return failed && !failed.ok ? { name, moved: false, reason: failed.reason, changes } : { name, moved: true, changes };
|
|
2602
|
+
});
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2579
2605
|
// src/commands.ts
|
|
2580
2606
|
var USAGE_DAYS = 30;
|
|
2581
2607
|
function parseArgs(argv) {
|
|
@@ -2583,7 +2609,7 @@ function parseArgs(argv) {
|
|
|
2583
2609
|
const days = daysIdx >= 0 ? Number(argv[daysIdx + 1]) : USAGE_DAYS;
|
|
2584
2610
|
const keepIdx = argv.indexOf("--keep");
|
|
2585
2611
|
return {
|
|
2586
|
-
positional: argv.filter((a, i) => !a.startsWith("--") &&
|
|
2612
|
+
positional: argv.filter((a, i) => !a.startsWith("--") && !["--days", "--keep", "--to"].includes(argv[i - 1])),
|
|
2587
2613
|
force: argv.includes("--force"),
|
|
2588
2614
|
all: argv.includes("--all"),
|
|
2589
2615
|
global: argv.includes("--global"),
|
|
@@ -2593,7 +2619,8 @@ function parseArgs(argv) {
|
|
|
2593
2619
|
days: Number.isFinite(days) && days > 0 ? days : USAGE_DAYS,
|
|
2594
2620
|
dryRun: argv.includes("--dry-run"),
|
|
2595
2621
|
allowGit: argv.includes("--allow-git"),
|
|
2596
|
-
...keepIdx >= 0 && argv[keepIdx + 1] ? { keep: argv[keepIdx + 1] } : {}
|
|
2622
|
+
...keepIdx >= 0 && argv[keepIdx + 1] ? { keep: argv[keepIdx + 1] } : {},
|
|
2623
|
+
to: argv.flatMap((a, i) => argv[i - 1] === "--to" && !a.startsWith("--") ? a.split(",").filter(Boolean) : [])
|
|
2597
2624
|
};
|
|
2598
2625
|
}
|
|
2599
2626
|
function colorState(state) {
|
|
@@ -2673,7 +2700,7 @@ async function status(args) {
|
|
|
2673
2700
|
const root = requireProject();
|
|
2674
2701
|
const skills = projectStatus(root);
|
|
2675
2702
|
const nested = nestedSkills(root);
|
|
2676
|
-
info(`${
|
|
2703
|
+
info(`${basename9(root)} ${dim(tildify(root))}
|
|
2677
2704
|
`);
|
|
2678
2705
|
if (skills.length === 0 && nested.length === 0) info("No skills in this project yet. Add one from your library with: skilllib add <name>");
|
|
2679
2706
|
const usage2 = await usageBySkill(args.days, root);
|
|
@@ -2750,7 +2777,7 @@ async function list(args) {
|
|
|
2750
2777
|
const installs = /* @__PURE__ */ new Map();
|
|
2751
2778
|
for (const project of visibleProjects()) {
|
|
2752
2779
|
for (const name of Object.keys(readManifest(project).skills)) {
|
|
2753
|
-
installs.set(name, [...installs.get(name) ?? [],
|
|
2780
|
+
installs.set(name, [...installs.get(name) ?? [], basename9(project)]);
|
|
2754
2781
|
}
|
|
2755
2782
|
}
|
|
2756
2783
|
const usage2 = await usageBySkill(args.days);
|
|
@@ -2787,7 +2814,7 @@ function projects(args) {
|
|
|
2787
2814
|
"projects",
|
|
2788
2815
|
roots.map(
|
|
2789
2816
|
(root) => ({
|
|
2790
|
-
name:
|
|
2817
|
+
name: basename9(root),
|
|
2791
2818
|
path: root,
|
|
2792
2819
|
skills: projectStatus(root).map(
|
|
2793
2820
|
(s) => ({ name: s.name, managed: s.managed, state: s.state, version: s.version, latest: s.latest })
|
|
@@ -2806,7 +2833,7 @@ function projects(args) {
|
|
|
2806
2833
|
const managed = skills.filter((s) => s.managed);
|
|
2807
2834
|
const attention = skills.filter((s) => s.state !== "ok" && s.state !== "local only").length;
|
|
2808
2835
|
return {
|
|
2809
|
-
Project:
|
|
2836
|
+
Project: basename9(root),
|
|
2810
2837
|
Path: dim(tildify(root)),
|
|
2811
2838
|
Skills: managed.map((s) => s.name).join(", ") || dim("none"),
|
|
2812
2839
|
"Local only": String(skills.filter((s) => s.state === "local only").length),
|
|
@@ -2840,13 +2867,13 @@ function sync(args) {
|
|
|
2840
2867
|
const changes = syncProject(root, { force: args.force });
|
|
2841
2868
|
if (changes.some((c) => c.action === "skipped")) process.exitCode = 1;
|
|
2842
2869
|
if (!args.json) {
|
|
2843
|
-
if (args.all) info(`${
|
|
2870
|
+
if (args.all) info(`${basename9(root)} ${dim(tildify(root))}`);
|
|
2844
2871
|
if (changes.length === 0) {
|
|
2845
2872
|
if (existsSync13(join14(root, MANIFEST_FILE))) success("Installed versions match skilllib.json");
|
|
2846
2873
|
else info(dim("No skilllib.json: nothing to sync"));
|
|
2847
2874
|
} else printChanges(root, changes, args);
|
|
2848
2875
|
}
|
|
2849
|
-
return { project:
|
|
2876
|
+
return { project: basename9(root), path: root, changes: withDirs(root, changes) };
|
|
2850
2877
|
});
|
|
2851
2878
|
if (args.json) args.all ? json("changes --all", results) : json("changes", results[0].changes);
|
|
2852
2879
|
if (!args.all) rememberProjects(roots);
|
|
@@ -2863,18 +2890,18 @@ function update(args) {
|
|
|
2863
2890
|
}
|
|
2864
2891
|
if (changes.some((c) => c.action === "skipped")) process.exitCode = 1;
|
|
2865
2892
|
if (!args.json) {
|
|
2866
|
-
if (args.all) info(`${
|
|
2893
|
+
if (args.all) info(`${basename9(root)} ${dim(tildify(root))}`);
|
|
2867
2894
|
if (changes.length === 0) success("Everything is on the latest version");
|
|
2868
2895
|
else printChanges(root, changes, args);
|
|
2869
2896
|
}
|
|
2870
|
-
return { project:
|
|
2897
|
+
return { project: basename9(root), path: root, changes: withDirs(root, changes) };
|
|
2871
2898
|
});
|
|
2872
2899
|
if (args.json) args.all ? json("changes --all", results) : json("changes", results[0].changes);
|
|
2873
2900
|
}
|
|
2874
2901
|
function outdated(args) {
|
|
2875
2902
|
const roots = args.all ? visibleProjects() : [requireProject()];
|
|
2876
2903
|
const behind = roots.flatMap(
|
|
2877
|
-
(root) => projectStatus(root).filter((s) => s.managed && s.latest !== null && s.version !== null && s.latest > s.version).map((s) => ({ project:
|
|
2904
|
+
(root) => projectStatus(root).filter((s) => s.managed && s.latest !== null && s.version !== null && s.latest > s.version).map((s) => ({ project: basename9(root), path: root, skill: s.name, installed: s.version, latest: s.latest, state: s.state }))
|
|
2878
2905
|
);
|
|
2879
2906
|
if (args.json) return json("outdated", behind);
|
|
2880
2907
|
const rows = behind.map((b) => ({ Project: b.project, Skill: b.skill, Installed: `v${b.installed}`, Latest: `v${b.latest}`, Status: colorState(b.state) }));
|
|
@@ -2908,7 +2935,7 @@ function importSkills(args) {
|
|
|
2908
2935
|
if (skillsDir === join14(root, ".claude", "skills") && isProjectCandidate(root)) {
|
|
2909
2936
|
if (readManifest(root).skills[name]?.hash !== treeHash(dir)) {
|
|
2910
2937
|
const change = addSkill(root, name);
|
|
2911
|
-
if (change.action !== "skipped") info(dim(` ${
|
|
2938
|
+
if (change.action !== "skipped") info(dim(` ${basename9(root)} now depends on ${name} v${change.to}`));
|
|
2912
2939
|
}
|
|
2913
2940
|
rememberProjects([root]);
|
|
2914
2941
|
}
|
|
@@ -2925,14 +2952,14 @@ function scan(args) {
|
|
|
2925
2952
|
const found = discoverProjects();
|
|
2926
2953
|
const library = new Set(librarySkills().map((s) => s.name));
|
|
2927
2954
|
const skillNames = (root) => [
|
|
2928
|
-
.../* @__PURE__ */ new Set([...ALL_PROJECT_DIRS.flatMap((d) => skillDirsIn(join14(root, d))).map((d) =>
|
|
2955
|
+
.../* @__PURE__ */ new Set([...ALL_PROJECT_DIRS.flatMap((d) => skillDirsIn(join14(root, d))).map((d) => basename9(d)), ...nestedSkills(root).map((s) => s.name)])
|
|
2929
2956
|
];
|
|
2930
2957
|
const withSkills = found.filter((root) => skillNames(root).length > 0);
|
|
2931
2958
|
info(`Scanned ${roots.map((r) => tildify(r)).join(", ")}: ${found.length} project${found.length === 1 ? "" : "s"}, ${withSkills.length} with skills
|
|
2932
2959
|
`);
|
|
2933
2960
|
table(
|
|
2934
2961
|
withSkills.map((root) => ({
|
|
2935
|
-
Project:
|
|
2962
|
+
Project: basename9(root),
|
|
2936
2963
|
Path: dim(tildify(root)),
|
|
2937
2964
|
Skills: skillNames(root).map((n) => library.has(n) ? n : yellow(n)).join(", ")
|
|
2938
2965
|
}))
|
|
@@ -3016,12 +3043,12 @@ function restore(args) {
|
|
|
3016
3043
|
process.exitCode = 1;
|
|
3017
3044
|
}
|
|
3018
3045
|
}
|
|
3019
|
-
async function
|
|
3046
|
+
async function allIssues(args, withUsage = true) {
|
|
3020
3047
|
const projects2 = visibleProjects();
|
|
3021
3048
|
const machine = machineSkills();
|
|
3022
3049
|
const libraryNames = new Set(librarySkills().map((s) => s.name));
|
|
3023
3050
|
const issues = findIssues(projects2, machine, libraryNames);
|
|
3024
|
-
if (enabledHarnesses().includes("claude-code")) {
|
|
3051
|
+
if (withUsage && enabledHarnesses().includes("claude-code")) {
|
|
3025
3052
|
const projectOf = projectOfFactory(projects2);
|
|
3026
3053
|
const uses = await scanUsage(args.days);
|
|
3027
3054
|
const inProject = usesByProject(uses, projectOf);
|
|
@@ -3034,6 +3061,10 @@ async function doctor(args) {
|
|
|
3034
3061
|
})
|
|
3035
3062
|
);
|
|
3036
3063
|
}
|
|
3064
|
+
return issues;
|
|
3065
|
+
}
|
|
3066
|
+
async function doctor(args) {
|
|
3067
|
+
const issues = await allIssues(args);
|
|
3037
3068
|
const fixed = (fix) => {
|
|
3038
3069
|
const r = runFix(fix);
|
|
3039
3070
|
if (!r.ok) process.exitCode = 1;
|
|
@@ -3059,13 +3090,38 @@ async function doctor(args) {
|
|
|
3059
3090
|
for (const issue of issues) {
|
|
3060
3091
|
info(`${issue.severity === "problem" ? red("\u25CF") : yellow("\u25C6")} ${issue.title}`);
|
|
3061
3092
|
info(dim(` ${issue.detail}${issue.fix ? ` \u2192 ${issue.fix.label}` : ""}`));
|
|
3062
|
-
if (issue.choices) info(dim(`
|
|
3093
|
+
if (issue.choices) info(dim(` your call: ${issue.choices.map((c, i) => `${i + 1}) ${c.label}`).join(" ")} \u2192 skilllib apply ${issue.id} <n>`));
|
|
3063
3094
|
if (args.fix && issue.fix) {
|
|
3064
3095
|
const r = fixed(issue.fix);
|
|
3065
3096
|
info(` ${r.ok ? green("\u2713") : red("\u2717")} ${r.message}`);
|
|
3066
3097
|
}
|
|
3067
3098
|
}
|
|
3068
|
-
if (!args.fix && issues.some((i) => i.fix)) info(dim("\nRun `skilllib doctor --fix` to apply the fixes, or
|
|
3099
|
+
if (!args.fix && issues.some((i) => i.fix)) info(dim("\nRun `skilllib doctor --fix` to apply the fixes, or one at a time with `skilllib apply <id>`."));
|
|
3100
|
+
}
|
|
3101
|
+
async function apply(args) {
|
|
3102
|
+
const [id, pick] = args.positional;
|
|
3103
|
+
if (!id) throw new Error("Usage: skilllib apply <issue-id> [choice] (ids and choices: skilllib status --json or doctor --json)");
|
|
3104
|
+
const found = [...statusIssues(findProjectRoot()), ...await allIssues(args, id.startsWith("unused:"))].filter((i) => i.id === id);
|
|
3105
|
+
if (!found.length) throw new Error(`No issue ${id} right now (it may be fixed already; for an unused:\u2026 issue, pass doctor's --days). See skilllib status --json or doctor --json`);
|
|
3106
|
+
const optionsOf = (i) => i.choices ?? (i.fix ? [i.fix] : []);
|
|
3107
|
+
const labels = (i) => optionsOf(i).map((c) => c.label).join("\0");
|
|
3108
|
+
const byLabel = pick === void 0 ? void 0 : found.find((i) => optionsOf(i).some((c) => c.label.toLowerCase() === pick.toLowerCase()));
|
|
3109
|
+
if (!byLabel && found.some((i) => labels(i) !== labels(found[0]))) {
|
|
3110
|
+
const all = [...new Set(found.flatMap((i) => optionsOf(i).map((c) => c.label)))];
|
|
3111
|
+
throw new Error(`${found[0].title}: its choices differ between this repo and all your repos; pick one by label: ${all.map((l) => `"${l}"`).join(" ")}`);
|
|
3112
|
+
}
|
|
3113
|
+
const issue = byLabel ?? found[0];
|
|
3114
|
+
const options = optionsOf(issue);
|
|
3115
|
+
if (!options.length) throw new Error(`${issue.title}: nothing to apply. ${issue.detail}`);
|
|
3116
|
+
const listed = options.map((c, i) => `${i + 1}) ${c.label}`).join(" ");
|
|
3117
|
+
if (pick === void 0 && options.length > 1) throw new Error(`${issue.title}: pick one: ${listed}`);
|
|
3118
|
+
const choice = pick === void 0 ? options[0] : options.find((c) => c.label.toLowerCase() === pick.toLowerCase()) ?? options[Number(pick) - 1];
|
|
3119
|
+
if (!choice) throw new Error(`${issue.title}: no choice ${pick}. Pick one: ${listed}`);
|
|
3120
|
+
const r = runFix(choice);
|
|
3121
|
+
if (!r.ok) process.exitCode = 1;
|
|
3122
|
+
if (args.json) return json("apply", { id, applied: choice.label, ...r });
|
|
3123
|
+
if (r.ok) success(r.message);
|
|
3124
|
+
else error(r.message);
|
|
3069
3125
|
}
|
|
3070
3126
|
async function usage(args) {
|
|
3071
3127
|
const summaries = summarize(await scanUsage(args.days), projectOfFactory(knownProjects()));
|
|
@@ -3074,7 +3130,7 @@ async function usage(args) {
|
|
|
3074
3130
|
return json("usage", {
|
|
3075
3131
|
usageDays: args.days,
|
|
3076
3132
|
source: "Claude Code transcripts",
|
|
3077
|
-
skills: summaries.map((s) => ({ skill: s.skill, uses: s.uses, lastUsed: s.lastUsed, projects: [...s.projects].map((p) =>
|
|
3133
|
+
skills: summaries.map((s) => ({ skill: s.skill, uses: s.uses, lastUsed: s.lastUsed, projects: [...s.projects].map((p) => basename9(p)), inLibrary: library2.has(s.skill) })),
|
|
3078
3134
|
unusedLibrarySkills: librarySkills().filter((l) => !summaries.some((u) => u.skill === l.name)).map((l) => l.name)
|
|
3079
3135
|
});
|
|
3080
3136
|
}
|
|
@@ -3089,7 +3145,7 @@ async function usage(args) {
|
|
|
3089
3145
|
summaries.map((s) => ({
|
|
3090
3146
|
Skill: s.skill,
|
|
3091
3147
|
Uses: String(s.uses),
|
|
3092
|
-
Projects: [...s.projects].map((p) =>
|
|
3148
|
+
Projects: [...s.projects].map((p) => basename9(p)).join(", ") || dim("-"),
|
|
3093
3149
|
"Last used": lastUsed(s),
|
|
3094
3150
|
Library: library.has(s.skill) ? green("yes") : dim("no")
|
|
3095
3151
|
}))
|
|
@@ -3115,7 +3171,7 @@ function show(args) {
|
|
|
3115
3171
|
path: dir,
|
|
3116
3172
|
latest: latestVersion(name)?.version ?? null,
|
|
3117
3173
|
origin: libraryOrigins()[name] ?? "",
|
|
3118
|
-
projects: knownProjects().filter((p) => name in readManifest(p).skills).map((p) =>
|
|
3174
|
+
projects: knownProjects().filter((p) => name in readManifest(p).skills).map((p) => basename9(p)),
|
|
3119
3175
|
skillMd: readFileSync9(join14(dir, "SKILL.md"), "utf-8")
|
|
3120
3176
|
});
|
|
3121
3177
|
}
|
|
@@ -3149,7 +3205,7 @@ function link(args) {
|
|
|
3149
3205
|
for (const root of roots) {
|
|
3150
3206
|
if (args.allowTracked) allowTrackedLinks(root);
|
|
3151
3207
|
const res = linkAll(root);
|
|
3152
|
-
const label = args.all ? `${
|
|
3208
|
+
const label = args.all ? `${basename9(root)}: ` : "";
|
|
3153
3209
|
if (res.linked.length) success(`${label}linked ${res.linked.length} skill(s) for your other agents`);
|
|
3154
3210
|
else if (!args.all && !res.blocked.length && !res.uncommitted.length) success("Every skill here is already usable by all your agents");
|
|
3155
3211
|
if (res.blocked.length) {
|
|
@@ -3165,6 +3221,7 @@ function link(args) {
|
|
|
3165
3221
|
function globalCommand(args) {
|
|
3166
3222
|
const [action, ...names] = args.positional;
|
|
3167
3223
|
const yours = machineSkills().filter((m) => m.movable && !m.broken);
|
|
3224
|
+
if (action === "move") return moveGlobal(args, names, yours);
|
|
3168
3225
|
if (action === "keep" || action === "unkeep") {
|
|
3169
3226
|
const unknown = names.filter((n) => !yours.some((m) => m.name === n));
|
|
3170
3227
|
if (!names.length || unknown.length) {
|
|
@@ -3174,7 +3231,7 @@ function globalCommand(args) {
|
|
|
3174
3231
|
setKeepGlobal(names, action === "keep");
|
|
3175
3232
|
success(action === "keep" ? `Keeping ${names.join(", ")} global on purpose` : `${names.join(", ")} will show up in cleanup again`);
|
|
3176
3233
|
} else if (action) {
|
|
3177
|
-
error("Usage: skilllib global [keep|unkeep <name>...]");
|
|
3234
|
+
error("Usage: skilllib global [keep|unkeep <name>...] | skilllib global move <name>... --to <repo>...");
|
|
3178
3235
|
process.exit(1);
|
|
3179
3236
|
}
|
|
3180
3237
|
const kept = keptGlobal();
|
|
@@ -3184,6 +3241,46 @@ function globalCommand(args) {
|
|
|
3184
3241
|
if (unreviewed) info(dim(`
|
|
3185
3242
|
${unreviewed} not reviewed. Keep one global on purpose: skilllib global keep <name>`));
|
|
3186
3243
|
}
|
|
3244
|
+
function repoRoot(repo) {
|
|
3245
|
+
const looksLikePath = /[\\/]/.test(repo) || repo.startsWith(".") || repo.startsWith("~");
|
|
3246
|
+
const named = looksLikePath ? [] : visibleProjects().filter((p) => basename9(p) === repo);
|
|
3247
|
+
if (named.length === 1) return named[0];
|
|
3248
|
+
if (named.length > 1) throw new Error(`Several projects are named ${repo}: ${named.map(tildify).join(", ")}. Pass its path.`);
|
|
3249
|
+
const path = expandHome(repo);
|
|
3250
|
+
if (!existsSync13(path)) throw new Error(`No project ${repo} (pass its path, or see skilllib projects)`);
|
|
3251
|
+
const root = projectHere(path);
|
|
3252
|
+
if (root !== path) throw new Error(root ? `${tildify(path)} is inside ${tildify(root)}: pass the repo's top folder` : `${tildify(path)} isn't in a git repo`);
|
|
3253
|
+
return root;
|
|
3254
|
+
}
|
|
3255
|
+
function moveGlobal(args, names, yours) {
|
|
3256
|
+
if (!names.length || !args.to.length) throw new Error("Usage: skilllib global move <name>... --to <repo>[,<repo>...]");
|
|
3257
|
+
const unknown = names.filter((n) => !yours.some((m) => m.name === n));
|
|
3258
|
+
if (unknown.length) throw new Error(`Not one of your global skills: ${unknown.join(", ")} (see skilllib global)`);
|
|
3259
|
+
const roots = [...new Set(args.to.map(repoRoot))];
|
|
3260
|
+
const skills = yours.filter((m) => names.includes(m.name));
|
|
3261
|
+
const moves = moveGlobalSkills(skills, roots, { allowTracked: args.allowTracked });
|
|
3262
|
+
if (moves.some((m) => !m.moved)) process.exitCode = 1;
|
|
3263
|
+
if (args.json) {
|
|
3264
|
+
return json(
|
|
3265
|
+
"global move",
|
|
3266
|
+
moves.map(({ name, moved, reason, changes }) => ({
|
|
3267
|
+
skill: name,
|
|
3268
|
+
moved,
|
|
3269
|
+
...reason ? { reason } : {},
|
|
3270
|
+
projects: changes.map(([root, c]) => ({ project: basename9(root), path: root, change: withDirs(root, [c])[0] }))
|
|
3271
|
+
}))
|
|
3272
|
+
);
|
|
3273
|
+
}
|
|
3274
|
+
for (const m of moves) {
|
|
3275
|
+
if (m.moved) success(`${m.name} now loads only in ${m.changes.map(([r]) => basename9(r)).join(", ")} (the global copy is in skilllib restore)`);
|
|
3276
|
+
else error(`${m.name}: ${m.reason}`);
|
|
3277
|
+
for (const [root, c] of m.changes) {
|
|
3278
|
+
if (c.blocked?.length) warn(` ${basename9(root)}: git tracks ${c.blocked.join(", ")}, so no link there. Re-run with --allow-tracked to add it.`);
|
|
3279
|
+
if (c.uncommitted?.length) warn(` ${basename9(root)}: no link in ${c.uncommitted.join(", ")}: git tracks it but not the skill's copy, so the link would break for teammates. Commit the copy, then run skilllib link --allow-tracked.`);
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
if (moves.some((m) => m.moved)) info(dim("Commit skilllib.json and the skill folders in each repo."));
|
|
3283
|
+
}
|
|
3187
3284
|
function tidy(args) {
|
|
3188
3285
|
const names = args.positional;
|
|
3189
3286
|
const keepFor = (base) => args.keep ? Object.fromEntries(names.map((n) => [n, base ? resolve7(base, args.keep) : expandHome(args.keep)])) : {};
|
|
@@ -3196,13 +3293,13 @@ function tidy(args) {
|
|
|
3196
3293
|
for (const name of names) {
|
|
3197
3294
|
const where = targets.map((root) => ({ root, ...foldersOf(root, name) })).filter((w) => w.all.length);
|
|
3198
3295
|
if (!where.length) {
|
|
3199
|
-
problems.push(`${name}: no skill by that name ${args.global ? "in your global folders" : args.all ? "in your projects" : `in ${
|
|
3296
|
+
problems.push(`${name}: no skill by that name ${args.global ? "in your global folders" : args.all ? "in your projects" : `in ${basename9(targets[0])}`}`);
|
|
3200
3297
|
continue;
|
|
3201
3298
|
}
|
|
3202
3299
|
if (!args.keep) continue;
|
|
3203
3300
|
for (const { root, real } of where) {
|
|
3204
3301
|
if (real.includes(keepFor(root)[name])) continue;
|
|
3205
|
-
const label = root ? `${
|
|
3302
|
+
const label = root ? `${basename9(root)} \xB7 ` : "";
|
|
3206
3303
|
problems.push(
|
|
3207
3304
|
`${label}${name}: no copy in ${args.keep}; ${real.length ? `pick one of: ${real.map((d) => folderLabel(root, d)).join(", ")}` : "it has no real copy to keep"}`
|
|
3208
3305
|
);
|
|
@@ -3213,7 +3310,7 @@ function tidy(args) {
|
|
|
3213
3310
|
process.exit(1);
|
|
3214
3311
|
}
|
|
3215
3312
|
const reports = targets.map(
|
|
3216
|
-
(root) => root === null ? { label: "global folders", report: planGlobalTidy({ keep: keepFor(null) }) } : { label:
|
|
3313
|
+
(root) => root === null ? { label: "global folders", report: planGlobalTidy({ keep: keepFor(null) }) } : { label: basename9(root), report: planProjectTidy(root, { keep: keepFor(root) }) }
|
|
3217
3314
|
);
|
|
3218
3315
|
let held = 0;
|
|
3219
3316
|
let changed = 0;
|
|
@@ -3347,6 +3444,7 @@ export {
|
|
|
3347
3444
|
copyLabel,
|
|
3348
3445
|
addedAt,
|
|
3349
3446
|
scanUsage,
|
|
3447
|
+
moveGlobalSkills,
|
|
3350
3448
|
parseArgs,
|
|
3351
3449
|
projectOfFactory,
|
|
3352
3450
|
status,
|
|
@@ -3365,6 +3463,7 @@ export {
|
|
|
3365
3463
|
newSkill,
|
|
3366
3464
|
restore,
|
|
3367
3465
|
doctor,
|
|
3466
|
+
apply,
|
|
3368
3467
|
usage,
|
|
3369
3468
|
show,
|
|
3370
3469
|
harnessesCommand,
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
add,
|
|
4
4
|
agentSkill,
|
|
5
|
+
apply,
|
|
5
6
|
doctor,
|
|
6
7
|
error,
|
|
7
8
|
folders,
|
|
@@ -27,7 +28,7 @@ import {
|
|
|
27
28
|
unhide,
|
|
28
29
|
update,
|
|
29
30
|
usage
|
|
30
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-LO4FMXDE.js";
|
|
31
32
|
|
|
32
33
|
// src/index.ts
|
|
33
34
|
import { readFileSync } from "fs";
|
|
@@ -43,6 +44,8 @@ Set up:
|
|
|
43
44
|
skilllib import <dir>... Add skill folders to the library (--force replaces)
|
|
44
45
|
skilllib import --global Copy your global skills (every agent's folder) into the library
|
|
45
46
|
skilllib global [keep|unkeep <name>...] Your global skills; mark ones you keep global on purpose
|
|
47
|
+
skilllib global move <name>... --to <repo>...
|
|
48
|
+
Load global skills only in those repos (by path or project name)
|
|
46
49
|
|
|
47
50
|
In a project (a git repo or a folder with skilllib.json; never your home folder):
|
|
48
51
|
skilllib status This project's skills, which agents load them, and issues
|
|
@@ -65,6 +68,7 @@ Library:
|
|
|
65
68
|
|
|
66
69
|
Health:
|
|
67
70
|
skilllib doctor [--fix] Broken links, duplicates, out-of-date projects
|
|
71
|
+
skilllib apply <id> [choice] Apply one issue's fix, or the choice you pick (number or label)
|
|
68
72
|
skilllib restore [name] Restore something skilllib moved out of the way
|
|
69
73
|
skilllib unhide <path> Show a hidden project again
|
|
70
74
|
|
|
@@ -73,7 +77,7 @@ Agents:
|
|
|
73
77
|
Teach your agents to use skilllib, so you can ask them
|
|
74
78
|
"which skills can you use here?" in any repo
|
|
75
79
|
--json Machine-readable output (status list show projects outdated
|
|
76
|
-
usage doctor add remove sync update import agent-skill)
|
|
80
|
+
usage doctor apply add remove sync update import global move agent-skill)
|
|
77
81
|
|
|
78
82
|
Options: --force (overwrite local edits) --allow-tracked (link into git-tracked skill folders) --days N
|
|
79
83
|
`.trim();
|
|
@@ -92,10 +96,10 @@ async function main() {
|
|
|
92
96
|
if (!["--version", "-v", "version", "help", "--help", "-h"].includes(command ?? "")) refreshAgentSkill();
|
|
93
97
|
switch (command) {
|
|
94
98
|
case void 0:
|
|
95
|
-
return process.stdin.isTTY && process.stdout.isTTY ? (await import("./tui-
|
|
99
|
+
return process.stdin.isTTY && process.stdout.isTTY ? (await import("./tui-3LDZ5GOZ.js")).tui() : overview(args);
|
|
96
100
|
case "ui":
|
|
97
101
|
if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("`skilllib ui` needs an interactive terminal. Agents and scripts: use `skilllib status --json`.");
|
|
98
|
-
return (await import("./tui-
|
|
102
|
+
return (await import("./tui-3LDZ5GOZ.js")).tui();
|
|
99
103
|
case "status":
|
|
100
104
|
return status(args);
|
|
101
105
|
case "add":
|
|
@@ -144,6 +148,8 @@ async function main() {
|
|
|
144
148
|
return restore(args);
|
|
145
149
|
case "doctor":
|
|
146
150
|
return doctor(args);
|
|
151
|
+
case "apply":
|
|
152
|
+
return apply(args);
|
|
147
153
|
case "usage":
|
|
148
154
|
return usage(args);
|
|
149
155
|
case "agent-skill":
|
package/dist/json.d.cts
CHANGED
|
@@ -70,10 +70,13 @@ type StatusGlobalGroup = {
|
|
|
70
70
|
};
|
|
71
71
|
/**
|
|
72
72
|
* Problems skilllib found here or in your global skills that share a cause and remedy: a `fix` is a
|
|
73
|
-
* safe repair (`skilllib doctor --fix` runs it); `choices` need the user to decide
|
|
73
|
+
* safe repair (`skilllib doctor --fix` runs it); `choices` need the user to decide.
|
|
74
|
+
* `skilllib apply <id> [choice]` applies either, one problem at a time.
|
|
74
75
|
*/
|
|
75
76
|
type StatusIssue = {
|
|
76
77
|
problems: string[];
|
|
78
|
+
/** One per problem, in the same order: what `skilllib apply` takes. */
|
|
79
|
+
ids: string[];
|
|
77
80
|
detail: string;
|
|
78
81
|
fix?: string;
|
|
79
82
|
choices?: string[];
|
|
@@ -167,7 +170,7 @@ type DoctorJson = {
|
|
|
167
170
|
detail: string;
|
|
168
171
|
/** A safe repair; `doctor --fix` runs it. */
|
|
169
172
|
fix?: string;
|
|
170
|
-
/** Fixes that need a decision
|
|
173
|
+
/** Fixes that need a decision: `skilllib apply <id> <n>` runs the nth (from 1). */
|
|
171
174
|
choices?: string[];
|
|
172
175
|
/** With --fix: what running `fix` did. */
|
|
173
176
|
fixed?: {
|
|
@@ -175,6 +178,25 @@ type DoctorJson = {
|
|
|
175
178
|
message: string;
|
|
176
179
|
};
|
|
177
180
|
}[];
|
|
181
|
+
/** `skilllib apply <issue-id> [choice] --json` */
|
|
182
|
+
type ApplyJson = {
|
|
183
|
+
id: string;
|
|
184
|
+
applied: string;
|
|
185
|
+
ok: boolean;
|
|
186
|
+
message: string;
|
|
187
|
+
};
|
|
188
|
+
/** `skilllib global move <name>... --to <repo>... --json`: one entry per skill. */
|
|
189
|
+
type GlobalMoveJson = {
|
|
190
|
+
skill: string;
|
|
191
|
+
/** Added to every repo and no longer global. When false, it still loads globally and `reason` says why. */
|
|
192
|
+
moved: boolean;
|
|
193
|
+
reason?: string;
|
|
194
|
+
projects: {
|
|
195
|
+
project: string;
|
|
196
|
+
path: string;
|
|
197
|
+
change: ChangeJson;
|
|
198
|
+
}[];
|
|
199
|
+
}[];
|
|
178
200
|
/** `skilllib usage [--days N] --json` */
|
|
179
201
|
type UsageJson = {
|
|
180
202
|
usageDays: number;
|
|
@@ -197,4 +219,4 @@ type AgentSkillJson = {
|
|
|
197
219
|
removed?: string[];
|
|
198
220
|
};
|
|
199
221
|
|
|
200
|
-
export type { AgentSkillJson, AgentSkillState, AllChangesJson, ChangeJson, ChangesJson, DoctorJson, GitState, HarnessId, ImportJson, ListJson, OutdatedJson, ProjectSource, ProjectsJson, ShowJson, SkillState, SourceKind, StatusGlobalGroup, StatusIssue, StatusJson, StatusSkillGroup, UsageJson };
|
|
222
|
+
export type { AgentSkillJson, AgentSkillState, AllChangesJson, ApplyJson, ChangeJson, ChangesJson, DoctorJson, GitState, GlobalMoveJson, HarnessId, ImportJson, ListJson, OutdatedJson, ProjectSource, ProjectsJson, ShowJson, SkillState, SourceKind, StatusGlobalGroup, StatusIssue, StatusJson, StatusSkillGroup, UsageJson };
|
package/dist/json.d.ts
CHANGED
|
@@ -70,10 +70,13 @@ type StatusGlobalGroup = {
|
|
|
70
70
|
};
|
|
71
71
|
/**
|
|
72
72
|
* Problems skilllib found here or in your global skills that share a cause and remedy: a `fix` is a
|
|
73
|
-
* safe repair (`skilllib doctor --fix` runs it); `choices` need the user to decide
|
|
73
|
+
* safe repair (`skilllib doctor --fix` runs it); `choices` need the user to decide.
|
|
74
|
+
* `skilllib apply <id> [choice]` applies either, one problem at a time.
|
|
74
75
|
*/
|
|
75
76
|
type StatusIssue = {
|
|
76
77
|
problems: string[];
|
|
78
|
+
/** One per problem, in the same order: what `skilllib apply` takes. */
|
|
79
|
+
ids: string[];
|
|
77
80
|
detail: string;
|
|
78
81
|
fix?: string;
|
|
79
82
|
choices?: string[];
|
|
@@ -167,7 +170,7 @@ type DoctorJson = {
|
|
|
167
170
|
detail: string;
|
|
168
171
|
/** A safe repair; `doctor --fix` runs it. */
|
|
169
172
|
fix?: string;
|
|
170
|
-
/** Fixes that need a decision
|
|
173
|
+
/** Fixes that need a decision: `skilllib apply <id> <n>` runs the nth (from 1). */
|
|
171
174
|
choices?: string[];
|
|
172
175
|
/** With --fix: what running `fix` did. */
|
|
173
176
|
fixed?: {
|
|
@@ -175,6 +178,25 @@ type DoctorJson = {
|
|
|
175
178
|
message: string;
|
|
176
179
|
};
|
|
177
180
|
}[];
|
|
181
|
+
/** `skilllib apply <issue-id> [choice] --json` */
|
|
182
|
+
type ApplyJson = {
|
|
183
|
+
id: string;
|
|
184
|
+
applied: string;
|
|
185
|
+
ok: boolean;
|
|
186
|
+
message: string;
|
|
187
|
+
};
|
|
188
|
+
/** `skilllib global move <name>... --to <repo>... --json`: one entry per skill. */
|
|
189
|
+
type GlobalMoveJson = {
|
|
190
|
+
skill: string;
|
|
191
|
+
/** Added to every repo and no longer global. When false, it still loads globally and `reason` says why. */
|
|
192
|
+
moved: boolean;
|
|
193
|
+
reason?: string;
|
|
194
|
+
projects: {
|
|
195
|
+
project: string;
|
|
196
|
+
path: string;
|
|
197
|
+
change: ChangeJson;
|
|
198
|
+
}[];
|
|
199
|
+
}[];
|
|
178
200
|
/** `skilllib usage [--days N] --json` */
|
|
179
201
|
type UsageJson = {
|
|
180
202
|
usageDays: number;
|
|
@@ -197,4 +219,4 @@ type AgentSkillJson = {
|
|
|
197
219
|
removed?: string[];
|
|
198
220
|
};
|
|
199
221
|
|
|
200
|
-
export type { AgentSkillJson, AgentSkillState, AllChangesJson, ChangeJson, ChangesJson, DoctorJson, GitState, HarnessId, ImportJson, ListJson, OutdatedJson, ProjectSource, ProjectsJson, ShowJson, SkillState, SourceKind, StatusGlobalGroup, StatusIssue, StatusJson, StatusSkillGroup, UsageJson };
|
|
222
|
+
export type { AgentSkillJson, AgentSkillState, AllChangesJson, ApplyJson, ChangeJson, ChangesJson, DoctorJson, GitState, GlobalMoveJson, HarnessId, ImportJson, ListJson, OutdatedJson, ProjectSource, ProjectsJson, ShowJson, SkillState, SourceKind, StatusGlobalGroup, StatusIssue, StatusJson, StatusSkillGroup, UsageJson };
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
linkGlobal,
|
|
37
37
|
listBackups,
|
|
38
38
|
machineSkills,
|
|
39
|
+
moveGlobalSkills,
|
|
39
40
|
planGlobalTidy,
|
|
40
41
|
planProjectTidy,
|
|
41
42
|
pluginBackups,
|
|
@@ -72,7 +73,7 @@ import {
|
|
|
72
73
|
versionHistory,
|
|
73
74
|
visibilityOf,
|
|
74
75
|
visibleProjects
|
|
75
|
-
} from "./chunk-
|
|
76
|
+
} from "./chunk-LO4FMXDE.js";
|
|
76
77
|
|
|
77
78
|
// src/tui/index.tsx
|
|
78
79
|
import { render } from "ink";
|
|
@@ -873,11 +874,14 @@ function originGroup(origin) {
|
|
|
873
874
|
return null;
|
|
874
875
|
}
|
|
875
876
|
var word = (name) => name.split(/[-_.]/)[0];
|
|
877
|
+
function libraryOrigin(w, l) {
|
|
878
|
+
return l && (l.origin ?? w.library.find((x) => x.origin?.match(/^plugin: ([^@]+)@/)?.[1] === l.name)?.origin);
|
|
879
|
+
}
|
|
876
880
|
function groupCandidates(w, u) {
|
|
877
881
|
const byTime = (family, installed) => installed ? [{ key: `time:${family}:${installed}`, label: `installed together ${installed.slice(0, 10)}` }] : [];
|
|
878
882
|
const byWord = (family, name) => word(name).length >= 3 ? [{ key: `word:${family}:${word(name)}`, label: `${word(name)}-*` }] : [];
|
|
879
883
|
if (!("source" in u)) {
|
|
880
|
-
const origin = originGroup(u
|
|
884
|
+
const origin = originGroup(libraryOrigin(w, u));
|
|
881
885
|
return [...origin ? [origin] : [], ...byWord("library", u.name)];
|
|
882
886
|
}
|
|
883
887
|
const m = u.machine;
|
|
@@ -892,8 +896,7 @@ function groupCandidates(w, u) {
|
|
|
892
896
|
return [...origin ? [origin] : [], ...byTime("global", m.installed), ...byWord("global", m.name)];
|
|
893
897
|
}
|
|
894
898
|
if (u.local?.source === "lib") {
|
|
895
|
-
const
|
|
896
|
-
const origin = originGroup(l?.origin);
|
|
899
|
+
const origin = originGroup(libraryOrigin(w, w.library.find((x) => x.name === u.name)));
|
|
897
900
|
return [...origin ? [origin] : [], ...byWord("local", u.name)];
|
|
898
901
|
}
|
|
899
902
|
return byWord("local", u.name);
|
|
@@ -920,7 +923,33 @@ function addRows(w, target, q) {
|
|
|
920
923
|
const taken = new Set(target ? project(w, target).skills.map((s) => s.name) : []);
|
|
921
924
|
const loaded = new Set(target ? usable(w, target).map((u) => u.name) : []);
|
|
922
925
|
const hit = (name, description) => !q || matches(name, q) || q.length >= 3 && description.toLowerCase().includes(q);
|
|
923
|
-
const
|
|
926
|
+
const groups = assignGroups(w.library.map((l) => groupCandidates(w, l)));
|
|
927
|
+
const shown = w.library.map((l, i) => ({ l, g: groups[i] })).filter(({ l }) => !taken.has(l.name) && hit(l.name, w.descriptions[l.name] ?? ""));
|
|
928
|
+
const skillRow = (l, inGroup) => ({
|
|
929
|
+
key: `lib:${l.name}`,
|
|
930
|
+
name: l.name,
|
|
931
|
+
note: `v${l.latest}`,
|
|
932
|
+
description: w.descriptions[l.name] ?? "",
|
|
933
|
+
members: [l.name],
|
|
934
|
+
...inGroup ? { inGroup } : {},
|
|
935
|
+
run: (w2) => w2.ops.add(target, l.name)
|
|
936
|
+
});
|
|
937
|
+
const library = [];
|
|
938
|
+
const done = /* @__PURE__ */ new Set();
|
|
939
|
+
for (const { l, g } of target ? shown : []) {
|
|
940
|
+
if (done.has(l.name)) continue;
|
|
941
|
+
const members = g ? shown.filter((x) => x.g?.key === g.key).map((x) => x.l) : [l];
|
|
942
|
+
members.forEach((m) => done.add(m.name));
|
|
943
|
+
if (members.length < 2) {
|
|
944
|
+
library.push(skillRow(l, false));
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
const names = members.map((m) => m.name);
|
|
948
|
+
library.push(
|
|
949
|
+
{ key: `group:${g.key}`, name: groupLabel(g, names), note: `${names.length} skills`, description: names.join(", "), members: names, run: (w2) => w2.ops.addTo([target], names) },
|
|
950
|
+
...members.map((m) => skillRow(m, true))
|
|
951
|
+
);
|
|
952
|
+
}
|
|
924
953
|
const exists = q !== "" && (loaded.has(q) || w.library.some((l) => l.name === q));
|
|
925
954
|
const where = target ? ` for ${target}` : "";
|
|
926
955
|
const fresh = exists ? [] : [
|
|
@@ -1682,7 +1711,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1682
1711
|
const repo = open;
|
|
1683
1712
|
const skills = project(world, repo).skills.map((s) => s.name);
|
|
1684
1713
|
return [
|
|
1685
|
-
row("#add", "+ Add a skill\u2026", ["Search your library, or create a new skill."], () => setModal({ kind: "add", cursor: 0, query: "", target: repo })),
|
|
1714
|
+
row("#add", "+ Add a skill\u2026", ["Search your library, or create a new skill."], () => setModal({ kind: "add", cursor: 0, query: "", target: repo, picked: /* @__PURE__ */ new Set() })),
|
|
1686
1715
|
...fix,
|
|
1687
1716
|
row(
|
|
1688
1717
|
"#repo",
|
|
@@ -1705,7 +1734,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1705
1734
|
}
|
|
1706
1735
|
if (place === "health") return fix;
|
|
1707
1736
|
if (place === "library")
|
|
1708
|
-
return [row("#add", "+ New skill\u2026", ["Create a skill in your library, or have an agent write it. Skills in your repos can be copied in from their own actions."], () => setModal({ kind: "add", cursor: 0, query: "", target: null }))];
|
|
1737
|
+
return [row("#add", "+ New skill\u2026", ["Create a skill in your library, or have an agent write it. Skills in your repos can be copied in from their own actions."], () => setModal({ kind: "add", cursor: 0, query: "", target: null, picked: /* @__PURE__ */ new Set() }))];
|
|
1709
1738
|
return [];
|
|
1710
1739
|
}
|
|
1711
1740
|
function move(delta) {
|
|
@@ -1963,6 +1992,18 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1963
1992
|
if (key.upArrow) return step(-1);
|
|
1964
1993
|
if (key.downArrow) return step(1);
|
|
1965
1994
|
if (key.backspace || key.delete) return setModal({ ...m, query: m.query.slice(0, -1), cursor: 0 });
|
|
1995
|
+
const members = rows[cursor2]?.members;
|
|
1996
|
+
if (input === " " && members) {
|
|
1997
|
+
const picked = new Set(m.picked);
|
|
1998
|
+
const all2 = members.every((n) => picked.has(n));
|
|
1999
|
+
for (const n of members) all2 ? picked.delete(n) : picked.add(n);
|
|
2000
|
+
return setModal({ ...m, picked });
|
|
2001
|
+
}
|
|
2002
|
+
if (key.return && m.target && m.picked.size) {
|
|
2003
|
+
const [target, names] = [m.target, [...m.picked]];
|
|
2004
|
+
setModal(null);
|
|
2005
|
+
return apply((w) => w.ops.addTo([target], names));
|
|
2006
|
+
}
|
|
1966
2007
|
if (key.return && rows[cursor2]?.agent) {
|
|
1967
2008
|
setModal(null);
|
|
1968
2009
|
return ui.copy(writeSkillPrompt(world, m.query, m.target), "a prompt for an agent to write the skill");
|
|
@@ -2170,10 +2211,15 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
2170
2211
|
);
|
|
2171
2212
|
} else if (modal?.kind === "add") {
|
|
2172
2213
|
const rows = addRows(world, modal.target, modal.query);
|
|
2214
|
+
const picked = modal.picked;
|
|
2215
|
+
const tick = (names) => {
|
|
2216
|
+
const n = names.filter((x) => picked.has(x)).length;
|
|
2217
|
+
return { text: n === names.length ? "[x] " : n ? "[-] " : "[ ] ", width: 4, color: n ? color.green : color.faint };
|
|
2218
|
+
};
|
|
2173
2219
|
body = /* @__PURE__ */ jsx2(
|
|
2174
2220
|
ListPanel,
|
|
2175
2221
|
{
|
|
2176
|
-
title: modal.target ? `Add a skill to ${modal.target}` : "New skill",
|
|
2222
|
+
title: modal.target ? picked.size ? `Add ${picked.size} skill${picked.size === 1 ? "" : "s"} to ${modal.target}` : `Add a skill to ${modal.target}` : "New skill",
|
|
2177
2223
|
focused: true,
|
|
2178
2224
|
width: columns,
|
|
2179
2225
|
height: bodyH,
|
|
@@ -2182,10 +2228,14 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
2182
2228
|
header: /* @__PURE__ */ jsx2(Text2, { color: modal.query ? color.accent : color.muted, children: modal.query ? `\u2315 ${modal.query}\u258F` : modal.target ? "type to search your library, or to name a new skill" : "type the new skill's name" }),
|
|
2183
2229
|
headerHeight: 1,
|
|
2184
2230
|
rows: rows.map(
|
|
2185
|
-
(r) => r.header ? { key: r.key, header: true, cells: [{ text: r.header }] } : r.create || r.agent ? { key: r.key, cells: [{ text: r.name, grow: true, color: color.accent }] } : {
|
|
2231
|
+
(r) => r.header ? { key: r.key, header: true, cells: [{ text: r.header }] } : r.create || r.agent ? { key: r.key, cells: [{ text: r.name, grow: true, color: color.accent }] } : r.members.length > 1 ? {
|
|
2232
|
+
key: r.key,
|
|
2233
|
+
cells: [tick(r.members), { text: clip(r.name, 30), width: 30, bold: true }, { text: `${r.note}: ${r.description}`, grow: true, color: color.faint }]
|
|
2234
|
+
} : {
|
|
2186
2235
|
key: r.key,
|
|
2187
2236
|
cells: [
|
|
2188
|
-
|
|
2237
|
+
tick(r.members),
|
|
2238
|
+
{ text: clip(`${r.inGroup ? " " : ""}${r.name}`, 24), width: 24 },
|
|
2189
2239
|
{ text: r.note, width: 6, color: color.accent },
|
|
2190
2240
|
{ text: r.description, grow: true, color: color.faint }
|
|
2191
2241
|
]
|
|
@@ -2200,7 +2250,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
2200
2250
|
list,
|
|
2201
2251
|
split ? side : null
|
|
2202
2252
|
] });
|
|
2203
|
-
const hints = modal?.kind === "confirm" ? "enter apply \xB7 esc cancel" : modal?.kind === "repos" || modal?.kind === "fixall" ? "space tick \xB7 enter apply \xB7 esc cancel" : modal?.kind === "agents" ? "space tick \xB7 enter save" : modal?.kind === "menu" ? "\u2191\u2193 choose \xB7 enter do it \xB7 esc cancel" : modal?.kind === "add" ? "enter add \xB7 esc cancel" : modal ? "any key closes" : focus === "detail" ? "\u2191\u2193 \u2190\u2192 choose \xB7 enter apply \xB7 esc back" : focus === "sidebar" ? "\u2191\u2193 choose \xB7 enter open \xB7 type to filter repos" : place === "projects" && !open ? `type to filter \xB7 enter open \xB7 ${sidebar ? "\u2190 sidebar" : "\u2190\u2192 places"} \xB7 ? keys` : place === "settings" ? `enter toggle \xB7 ${sidebar ? "\u2190 sidebar" : "\u2190\u2192 places"} \xB7 ? keys` : `type to filter \xB7 space \u2605 fix \xB7 enter actions \xB7 tab tabs${sidebar ? " \xB7 esc back" : `${dashboard ? " \xB7 esc all projects" : ""} \xB7 ? keys`}`;
|
|
2253
|
+
const hints = modal?.kind === "confirm" ? "enter apply \xB7 esc cancel" : modal?.kind === "repos" || modal?.kind === "fixall" ? "space tick \xB7 enter apply \xB7 esc cancel" : modal?.kind === "agents" ? "space tick \xB7 enter save" : modal?.kind === "menu" ? "\u2191\u2193 choose \xB7 enter do it \xB7 esc cancel" : modal?.kind === "add" ? modal.target ? "space tick \xB7 enter add \xB7 esc cancel" : "enter add \xB7 esc cancel" : modal ? "any key closes" : focus === "detail" ? "\u2191\u2193 \u2190\u2192 choose \xB7 enter apply \xB7 esc back" : focus === "sidebar" ? "\u2191\u2193 choose \xB7 enter open \xB7 type to filter repos" : place === "projects" && !open ? `type to filter \xB7 enter open \xB7 ${sidebar ? "\u2190 sidebar" : "\u2190\u2192 places"} \xB7 ? keys` : place === "settings" ? `enter toggle \xB7 ${sidebar ? "\u2190 sidebar" : "\u2190\u2192 places"} \xB7 ? keys` : `type to filter \xB7 space \u2605 fix \xB7 enter actions \xB7 tab tabs${sidebar ? " \xB7 esc back" : `${dashboard ? " \xB7 esc all projects" : ""} \xB7 ? keys`}`;
|
|
2204
2254
|
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: termCols, height: termRows, children: [
|
|
2205
2255
|
sidebar ? null : topBar,
|
|
2206
2256
|
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
@@ -2655,29 +2705,19 @@ function realOps(roots, backups) {
|
|
|
2655
2705
|
const moved = [];
|
|
2656
2706
|
const notes = [];
|
|
2657
2707
|
const held = [];
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
}
|
|
2663
|
-
if (group) recordOrigin(m.name, `group: ${group}`);
|
|
2664
|
-
const results = repos.map((r2) => [r2, addSkill(rootOf(r2), m.name)]);
|
|
2665
|
-
const skipped = results.filter(([, c]) => c.action === "skipped");
|
|
2666
|
-
if (skipped.length) {
|
|
2667
|
-
notes.push(`${m.name}: not added to ${skipped.map(([r2, c]) => `${r2} (${c.reason})`).join(", ")}, so it still loads globally`);
|
|
2668
|
-
continue;
|
|
2669
|
-
}
|
|
2670
|
-
const r = unloadGlobal(m.path, m.links);
|
|
2671
|
-
if (!r.ok) {
|
|
2672
|
-
notes.push(`${m.name}: ${r.reason}`);
|
|
2708
|
+
const repoRoots = repos.map(rootOf);
|
|
2709
|
+
for (const mv of moveGlobalSkills(skills, repoRoots, { group, label: (root) => repos[repoRoots.indexOf(root)] })) {
|
|
2710
|
+
if (!mv.moved) {
|
|
2711
|
+
notes.push(`${mv.name}: ${mv.reason}`);
|
|
2673
2712
|
continue;
|
|
2674
2713
|
}
|
|
2675
|
-
moved.push(
|
|
2676
|
-
for (const [
|
|
2677
|
-
const
|
|
2714
|
+
moved.push(mv.name);
|
|
2715
|
+
for (const [i, [root, c]] of mv.changes.entries()) {
|
|
2716
|
+
const repo = repos[i];
|
|
2717
|
+
const blind = visibilityOf(root, mv.name).flatMap((v) => v.paths === 0 ? [v.id] : []);
|
|
2678
2718
|
const blocked = [...c.blocked ?? [], ...c.uncommitted ?? []];
|
|
2679
2719
|
if (!blocked.length || !blind.length) continue;
|
|
2680
|
-
held.push({ repo, name:
|
|
2720
|
+
held.push({ repo, name: mv.name, blocked, blind, copy: copyDir(repo, mv.name), shared: !c.uncommitted?.length });
|
|
2681
2721
|
}
|
|
2682
2722
|
}
|
|
2683
2723
|
const gaps = /* @__PURE__ */ new Map();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skilllib",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.1",
|
|
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, Zed and Grok.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-skills",
|
package/skills/skilllib/SKILL.md
CHANGED
|
@@ -14,25 +14,32 @@ Always pass `--json` (one line of JSON on stdout; messages go to stderr). Never
|
|
|
14
14
|
|
|
15
15
|
The **Now** section at the end has this repo's `status` and `list` output. Answer from it and don't run them again. If it shows the commands instead of their output, run both in parallel.
|
|
16
16
|
|
|
17
|
-
`status --json`: `skills` are this repo's own; `nested` (when there are any) are in subfolders such as monorepo packages, and load only when the agent works in their `dir`; `global` groups load in every repo, as this repo loads them (its `.claude/settings.json` can turn a plugin on or off); `issues` are everything skilllib finds wrong (out-of-date or missing skills, broken links, skills loaded twice), with either a `fix` (a safe repair; `skilllib doctor --fix` applies every fix, across all the user's projects) or `choices` (decisions the user
|
|
17
|
+
`status --json`: `skills` are this repo's own; `nested` (when there are any) are in subfolders such as monorepo packages, and load only when the agent works in their `dir`; `global` groups load in every repo, as this repo loads them (its `.claude/settings.json` can turn a plugin on or off); `issues` are everything skilllib finds wrong (out-of-date or missing skills, broken links, skills loaded twice), one `ids` entry per `problems` entry, with either a `fix` (a safe repair; `skilllib doctor --fix` applies every fix, across all the user's projects) or `choices` (decisions: propose one, and apply it once the user agrees, see Changing). When asked what's out of date or broken, `issues` and each skill's `state` are the answer. Don't audit skill contents unless asked.
|
|
18
18
|
|
|
19
19
|
- `source`: `library` (added with skilllib) · `repo` (the team's: committed to git, or in `.agents/skills`) · `local` (only on this machine: not committed, not managed by skilllib) · `global` (`~/.claude/skills`, `~/.agents/skills` or `~/.codex/skills`) · `skills.sh` (`npx skills add`; `from` is the source repo) · `plugin` (Claude Code plugin; turn off with `/plugin`) · `claude.ai` (synced from the account) · `built-in` (bundled with an agent) · `skilllib` (this skill; `skilllib agent-skill remove` takes it out).
|
|
20
20
|
- Missing fields mean the usual: loaded by all of `agents`, state ok, in `.claude/skills`. `usesHere` counts Claude Code uses in this repo.
|
|
21
21
|
|
|
22
|
-
`list --json`: `inThisProject` (names) and `skills`, the library skills the repo doesn't have, each with a short `description` and the `projects` using it. To recommend, read the repo's README and manifests (see `files` in `status`) in one parallel step, then give a reason for each pick. Don't add a skill that already loads globally (it would load twice):
|
|
22
|
+
`list --json`: `inThisProject` (names) and `skills`, the library skills the repo doesn't have, each with a short `description` and the `projects` using it. To recommend, read the repo's README and manifests (see `files` in `status`) in one parallel step, then give a reason for each pick. Don't add a skill that already loads globally (it would load twice): offer to move it into just the repos that need it instead (`global move`). `show <name> --json` has the full SKILL.md.
|
|
23
23
|
|
|
24
|
-
Also: `usage --json` (Claude Code usage only, so "unused" is a hint), `projects --json` (every repo), `outdated --all --json
|
|
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
26
|
## Changing (ask first unless the user asked for this exact change)
|
|
27
27
|
|
|
28
|
-
`add <name>...` · `remove <name>...` · `update [name...]` · `sync` (pinned versions) · `import <dir>` (into the library) · `doctor --fix`. Pass several names in one call.
|
|
28
|
+
`add <name>...` · `remove <name>...` · `update [name...]` · `sync` (pinned versions) · `import <dir>` (into the library) · `doctor --fix`. Pass several names in one call. Their result is `[{name, action, from, to, dirs, reason, blocked, uncommitted, backedUp}]`, which is the whole result: there's no need to check files afterwards.
|
|
29
29
|
|
|
30
30
|
- `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
31
|
- `blocked`: git tracks that folder. Re-run with `--allow-tracked` only with their OK.
|
|
32
32
|
- `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.
|
|
33
33
|
- Changes apply from the agent's next session. Remind the user to commit `skilllib.json` and `.claude/skills`.
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
**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:
|
|
36
|
+
|
|
37
|
+
- `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, projects: [{project, path, change}]}]`; `moved: false` means it still loads globally, and `reason` says why.
|
|
38
|
+
- `global keep <name>...`: the user wants it everywhere; skilllib stops flagging it.
|
|
39
|
+
|
|
40
|
+
**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.
|
|
41
|
+
|
|
42
|
+
**Importing.** `import <dir>...` copies skill folders (each with a `SKILL.md`) into the library; skilllib doesn't download anything. For a skill on GitHub, clone the repo into a temporary folder, `import` the skill's folder, then `add` it here.
|
|
36
43
|
|
|
37
44
|
## Now
|
|
38
45
|
|