skilllib 1.11.0 → 1.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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
  ![agents](https://img.shields.io/badge/works%20with-Claude%20Code%20%C2%B7%20Cursor%20%C2%B7%20Codex%20%C2%B7%20Zed%20%C2%B7%20Grok-c792ea)
13
13
 
14
14
  ```bash
15
- npx skilllib
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
- - **💬 Talk to your agents about your skills.** Ask Claude Code, Cursor, Codex or Grok "which skills can you use here, and where do they come from?" or "which of my skills should this repo use?". They answer with skilllib.
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 # or: npx skilllib
68
- skilllib
73
+ npm install -g skilllib
74
+ skilllib agent-skill install
69
75
  ```
70
76
 
71
- 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.
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
- ## Manual
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
 
@@ -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)
@@ -385,7 +397,7 @@ The repo commands (`status`, `add`, `remove`, `sync`, `outdated`, `update`, `lin
385
397
 
386
398
  Flags: `--force` (overwrite local edits, backed up to `~/.skilllib/edit-backup`), `--allow-tracked` (link into git-tracked folders; skills git doesn't share are still left out), `--days N`, `--json`. For `tidy`: `--dry-run` (preview), `--allow-git` (also change committed files), `<name> --keep <folder>` (the copy that wins when copies differ).
387
399
 
388
- 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.
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. An unknown or misspelled flag is an error too, so a typo never runs silently. With `--json`, a failure prints `{"error": "…"}` on stdout as well as the message on stderr.
389
401
 
390
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:
391
403
 
@@ -442,7 +454,7 @@ Environment variables: `SKILLLIB_HOME` moves `~/.skilllib`, `$VISUAL` / `$EDITOR
442
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.
443
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.
444
456
  - Unloading a `npx skills` skill doesn't update that tool's lock file.
445
- - Installing new skills straight from GitHub is coming. For now, `skilllib import <folder>` or copy one from a repo.
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.
446
458
 
447
459
  ---
448
460
 
@@ -47,6 +47,7 @@ var jsonMode = false;
47
47
  function setJsonMode(on) {
48
48
  jsonMode = on;
49
49
  }
50
+ var isJsonMode = () => jsonMode;
50
51
  function json(output, data) {
51
52
  process.stdout.write(JSON.stringify(data) + "\n");
52
53
  }
@@ -219,7 +220,8 @@ function isSkillDir(dir) {
219
220
  function listFiles(dir) {
220
221
  return readdirSync(dir).filter((entry) => !IGNORED.has(entry)).flatMap((entry) => {
221
222
  const full = join3(dir, entry);
222
- return statSync(full).isDirectory() ? listFiles(full) : [full];
223
+ const st = statSync(full, { throwIfNoEntry: false });
224
+ return !st ? [] : st.isDirectory() ? listFiles(full) : [full];
223
225
  });
224
226
  }
225
227
  function treeHash(dir) {
@@ -232,8 +234,9 @@ function treeHash(dir) {
232
234
  return hash.digest("hex").slice(0, 16);
233
235
  }
234
236
  function copySkill(from, to) {
237
+ const source = realpathSync2(from);
235
238
  rmSync(to, { recursive: true, force: true });
236
- cpSync(realpathSync2(from), to, { recursive: true, dereference: true, filter: (src) => !IGNORED.has(basename(src)) });
239
+ cpSync(source, to, { recursive: true, dereference: true, filter: (src) => !IGNORED.has(basename(src)) && existsSync2(src) });
237
240
  }
238
241
  function skillDirsIn(dir) {
239
242
  if (!existsSync2(dir)) return [];
@@ -461,7 +464,7 @@ function setKeepGlobal(names, keep) {
461
464
 
462
465
  // src/library.ts
463
466
  import { execFileSync } from "child_process";
464
- import { existsSync as existsSync7, lstatSync as lstatSync2, mkdirSync as mkdirSync5, readdirSync as readdirSync4, readFileSync as readFileSync6, realpathSync as realpathSync4, renameSync, rmSync as rmSync2, symlinkSync, unlinkSync, writeFileSync as writeFileSync5 } from "fs";
467
+ import { cpSync as cpSync2, existsSync as existsSync7, lstatSync as lstatSync2, mkdirSync as mkdirSync5, readdirSync as readdirSync4, readFileSync as readFileSync6, realpathSync as realpathSync4, renameSync, rmSync as rmSync2, symlinkSync, unlinkSync, writeFileSync as writeFileSync5 } from "fs";
465
468
  import { basename as basename3, dirname as dirname2, join as join8, relative as relative2, sep as sep3 } from "path";
466
469
 
467
470
  // src/sources.ts
@@ -1032,25 +1035,28 @@ function removeSkill(root, name, { force = false } = {}) {
1032
1035
  return { name, action: "removed", ...backedUp ? { backedUp } : {} };
1033
1036
  }
1034
1037
  function syncProject(root, { force = false } = {}) {
1035
- const { skills } = readManifest(root);
1036
- return Object.entries(skills).flatMap(([name, dep]) => {
1037
- const local = treeHash(join8(root, dep.dir ?? PROJECT_SKILLS_DIR, name));
1038
- const linksOk = (dep.links ?? []).every((d) => existsSync7(join8(root, d, name, "SKILL.md")));
1039
- if (local === dep.hash && linksOk) return [];
1040
- const pinned = dep.version > 0 && getVersion(name, dep.version) ? dep.version : void 0;
1041
- if (local !== null && local !== dep.hash && !force) return [{ name, action: "skipped", reason: "has local edits (use --force to reset them)" }];
1042
- const change = addSkill(root, name, { force: true, version: pinned });
1043
- if (change.action === "skipped") return [{ ...change, reason: `not in your library (${tildify(libraryDir())}); import it, or set SKILLLIB_HOME to the library that has it` }];
1044
- return [{ ...change, action: local === null ? "installed" : change.action }];
1045
- });
1038
+ return Object.keys(readManifest(root).skills).flatMap((name) => syncSkill(root, name, { force }));
1039
+ }
1040
+ function syncSkill(root, name, { force = false } = {}) {
1041
+ const dep = readManifest(root).skills[name];
1042
+ if (!dep) return [];
1043
+ const local = treeHash(join8(root, dep.dir ?? PROJECT_SKILLS_DIR, name));
1044
+ const linksOk = (dep.links ?? []).every((d) => existsSync7(join8(root, d, name, "SKILL.md")));
1045
+ if (local === dep.hash && linksOk) return [];
1046
+ if (local !== null && local !== dep.hash && !force) return [{ name, action: "skipped", reason: "has local edits (use --force to reset them)" }];
1047
+ const change = addSkill(root, name, { force: true, version: pinnedVersion(name, dep) });
1048
+ if (change.action === "skipped") return [{ ...change, reason: `not in your library (${tildify(libraryDir())}); import it, or set SKILLLIB_HOME to the library that has it` }];
1049
+ return [{ ...change, action: local === null ? "installed" : change.action }];
1050
+ }
1051
+ function pinnedVersion(name, dep) {
1052
+ return versionForHash(name, dep.hash)?.version ?? (dep.version > 0 && getVersion(name, dep.version) ? dep.version : void 0);
1046
1053
  }
1047
1054
  function discardEdits(root, name) {
1048
1055
  const dep = readManifest(root).skills[name];
1049
1056
  if (!dep) return { name, action: "skipped", reason: "not managed by skilllib in this project" };
1050
1057
  const dir = join8(root, dep.dir ?? PROJECT_SKILLS_DIR, name);
1051
- const pinned = dep.version > 0 && getVersion(name, dep.version) ? dep.version : void 0;
1052
1058
  if (existsSync7(dir) && !isLink(dir)) stash(dir, "edit-backup");
1053
- return addSkill(root, name, { force: true, version: pinned });
1059
+ return addSkill(root, name, { force: true, version: pinnedVersion(name, dep) });
1054
1060
  }
1055
1061
  function updateProject(root, names, { force = false } = {}) {
1056
1062
  const { skills } = readManifest(root);
@@ -1090,6 +1096,15 @@ function readBackupIndex(bucket) {
1090
1096
  return {};
1091
1097
  }
1092
1098
  }
1099
+ function move(from, to) {
1100
+ try {
1101
+ renameSync(from, to);
1102
+ } catch (err) {
1103
+ if (err.code !== "EXDEV") throw err;
1104
+ cpSync2(from, to, { recursive: true, verbatimSymlinks: true });
1105
+ rmSync2(from, { recursive: true, force: true });
1106
+ }
1107
+ }
1093
1108
  function stash(dir, bucket) {
1094
1109
  let at = Date.now();
1095
1110
  const entryAt = (ms) => `${basename3(dir)}-${new Date(ms).toISOString().replace(/[:.]/g, "-")}`;
@@ -1097,7 +1112,7 @@ function stash(dir, bucket) {
1097
1112
  const entry = entryAt(at);
1098
1113
  const target = join8(skilllibHome(), bucket, entry);
1099
1114
  mkdirSync5(join8(skilllibHome(), bucket), { recursive: true });
1100
- renameSync(dir, target);
1115
+ move(dir, target);
1101
1116
  writeFileSync5(backupIndexFile(bucket), JSON.stringify({ ...readBackupIndex(bucket), [entry]: dir }, null, 2) + "\n");
1102
1117
  return target;
1103
1118
  }
@@ -1195,7 +1210,7 @@ function restoreBackup(backup) {
1195
1210
  }
1196
1211
  if (taken) return { ok: false, reason: `${backup.name} already exists there` };
1197
1212
  mkdirSync5(join8(to, ".."), { recursive: true });
1198
- renameSync(backup.path, to);
1213
+ move(backup.path, to);
1199
1214
  forgetLatest(backup.name);
1200
1215
  return { ok: true, to };
1201
1216
  }
@@ -1265,6 +1280,7 @@ function refreshAgentSkill() {
1265
1280
 
1266
1281
  // src/commands.ts
1267
1282
  import { existsSync as existsSync13, readFileSync as readFileSync9 } from "fs";
1283
+ import { parseArgs as parseFlags } from "util";
1268
1284
  import { basename as basename9, join as join14, resolve as resolve7, sep as sep5 } from "path";
1269
1285
 
1270
1286
  // src/git.ts
@@ -2604,23 +2620,35 @@ function moveGlobalSkills(skills, roots, { group, allowTracked = false, label =
2604
2620
 
2605
2621
  // src/commands.ts
2606
2622
  var USAGE_DAYS = 30;
2623
+ var FLAGS = {
2624
+ force: { type: "boolean" },
2625
+ all: { type: "boolean" },
2626
+ global: { type: "boolean" },
2627
+ fix: { type: "boolean" },
2628
+ "allow-tracked": { type: "boolean" },
2629
+ json: { type: "boolean" },
2630
+ "dry-run": { type: "boolean" },
2631
+ "allow-git": { type: "boolean" },
2632
+ days: { type: "string" },
2633
+ keep: { type: "string" },
2634
+ to: { type: "string", multiple: true }
2635
+ };
2607
2636
  function parseArgs(argv) {
2608
- const daysIdx = argv.indexOf("--days");
2609
- const days = daysIdx >= 0 ? Number(argv[daysIdx + 1]) : USAGE_DAYS;
2610
- const keepIdx = argv.indexOf("--keep");
2637
+ const { values: v, positionals } = parseFlags({ args: argv, options: FLAGS, allowPositionals: true, strict: true });
2638
+ const days = Number(v.days ?? USAGE_DAYS);
2611
2639
  return {
2612
- positional: argv.filter((a, i) => !a.startsWith("--") && !["--days", "--keep", "--to"].includes(argv[i - 1])),
2613
- force: argv.includes("--force"),
2614
- all: argv.includes("--all"),
2615
- global: argv.includes("--global"),
2616
- fix: argv.includes("--fix"),
2617
- allowTracked: argv.includes("--allow-tracked"),
2618
- json: argv.includes("--json"),
2640
+ positional: positionals,
2641
+ force: !!v.force,
2642
+ all: !!v.all,
2643
+ global: !!v.global,
2644
+ fix: !!v.fix,
2645
+ allowTracked: !!v["allow-tracked"],
2646
+ json: !!v.json,
2619
2647
  days: Number.isFinite(days) && days > 0 ? days : USAGE_DAYS,
2620
- dryRun: argv.includes("--dry-run"),
2621
- allowGit: argv.includes("--allow-git"),
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) : [])
2648
+ dryRun: !!v["dry-run"],
2649
+ allowGit: !!v["allow-git"],
2650
+ ...v.keep ? { keep: v.keep } : {},
2651
+ to: (v.to ?? []).flatMap((t) => t.split(",").filter(Boolean))
2624
2652
  };
2625
2653
  }
2626
2654
  function colorState(state) {
@@ -2844,8 +2872,7 @@ function projects(args) {
2844
2872
  }
2845
2873
  function add(args) {
2846
2874
  if (args.positional.length === 0) {
2847
- error("Usage: skilllib add <skill-name>... [--force]");
2848
- process.exit(1);
2875
+ throw new Error("Usage: skilllib add <skill-name>... [--force]");
2849
2876
  }
2850
2877
  const root = requireProject();
2851
2878
  if (args.allowTracked) allowTrackedLinks(root);
@@ -2854,13 +2881,13 @@ function add(args) {
2854
2881
  }
2855
2882
  function remove(args) {
2856
2883
  if (args.positional.length === 0) {
2857
- error("Usage: skilllib remove <skill-name>... [--force]");
2858
- process.exit(1);
2884
+ throw new Error("Usage: skilllib remove <skill-name>... [--force]");
2859
2885
  }
2860
2886
  const root = requireProject();
2861
2887
  printChanges(root, args.positional.map((name) => removeSkill(root, name, { force: args.force })), args);
2862
2888
  }
2863
2889
  function sync(args) {
2890
+ if (args.positional.length) throw new Error(`sync takes no skill names: it installs exactly what skilllib.json pins. To add a skill: skilllib add ${args.positional.join(" ")}`);
2864
2891
  const roots = args.all ? visibleProjects() : [requireProject()];
2865
2892
  const results = roots.map((root) => {
2866
2893
  if (args.allowTracked) allowTrackedLinks(root);
@@ -2913,20 +2940,21 @@ function outdated(args) {
2913
2940
  function importSkills(args) {
2914
2941
  const dirs = args.global ? machineSkills().filter((m) => m.movable && !m.broken).map((m) => m.path) : args.positional.map((p) => resolve7(p));
2915
2942
  if (dirs.length === 0) {
2916
- error("Usage: skilllib import <skill-dir>... [--force] | skilllib import --global");
2917
- process.exit(1);
2943
+ throw new Error("Usage: skilllib import <skill-dir>... [--force] | skilllib import --global");
2918
2944
  }
2919
2945
  const results = [];
2920
2946
  for (const dir of dirs) {
2921
2947
  if (!isSkillDir(dir)) {
2922
2948
  warn(`${tildify(dir)}: no SKILL.md, skipping`);
2923
2949
  results.push({ path: dir, status: "no SKILL.md" });
2950
+ process.exitCode = 1;
2924
2951
  continue;
2925
2952
  }
2926
2953
  const { name, status: result } = importSkill(dir, { force: args.force });
2927
2954
  results.push({ path: dir, name, status: result });
2928
2955
  if (result === "exists") {
2929
2956
  warn(`${name}: the library already has a different version (use --force to replace it)`);
2957
+ process.exitCode = 1;
2930
2958
  continue;
2931
2959
  }
2932
2960
  success(`${name} ${dim(result === "unchanged" ? "already in library" : result === "added" ? "added to library" : "library updated")}`);
@@ -2990,12 +3018,11 @@ function folders(args) {
2990
3018
  info(roots.length ? `Project folders:
2991
3019
  ${roots.map((r) => ` ${tildify(r)}`).join("\n")}` : "No project folders. Add one: skilllib folders add ~/Projects");
2992
3020
  if (hidden.length) info(dim(`Hidden projects: ${hidden.map((h) => tildify(h)).join(", ")} (skilllib unhide <path>)`));
2993
- if (failed) process.exit(1);
3021
+ if (failed) process.exitCode = 1;
2994
3022
  }
2995
3023
  function unhide(args) {
2996
3024
  if (args.positional.length === 0) {
2997
- error("Usage: skilllib unhide <path>...");
2998
- process.exit(1);
3025
+ throw new Error("Usage: skilllib unhide <path>...");
2999
3026
  }
3000
3027
  let failed = false;
3001
3028
  for (const p of args.positional) {
@@ -3007,18 +3034,16 @@ function unhide(args) {
3007
3034
  failed = true;
3008
3035
  }
3009
3036
  }
3010
- if (failed) process.exit(1);
3037
+ if (failed) process.exitCode = 1;
3011
3038
  }
3012
3039
  function newSkill(args) {
3013
3040
  const [name, ...rest] = args.positional;
3014
3041
  if (!name) {
3015
- error("Usage: skilllib new <name> [description]");
3016
- process.exit(1);
3042
+ throw new Error("Usage: skilllib new <name> [description]");
3017
3043
  }
3018
3044
  const r = createSkill(name, rest.join(" "));
3019
3045
  if (!r.ok) {
3020
- error(`Can't create ${name}: ${r.reason}`);
3021
- process.exit(1);
3046
+ throw new Error(`Can't create ${name}: ${r.reason}`);
3022
3047
  }
3023
3048
  success(`Created ${tildify(join14(r.dir, "SKILL.md"))}`);
3024
3049
  }
@@ -3033,8 +3058,7 @@ function restore(args) {
3033
3058
  }
3034
3059
  const backup = backups.find((b) => b.name === name);
3035
3060
  if (!backup) {
3036
- error(`No backup named ${name}`);
3037
- process.exit(1);
3061
+ throw new Error(`No backup named ${name}`);
3038
3062
  }
3039
3063
  const r = backup.kind === "plugin" ? restorePlugin(backup) : restoreBackup(backup);
3040
3064
  if (r.ok) success(`Restored ${name} to ${tildify(r.to)}`);
@@ -3160,8 +3184,7 @@ function show(args) {
3160
3184
  const name = args.positional[0];
3161
3185
  const dir = name ? join14(libraryDir(), name) : "";
3162
3186
  if (!name || !isSkillDir(dir)) {
3163
- error(name ? `${name} is not in the library` : "Usage: skilllib show <skill-name>");
3164
- process.exit(1);
3187
+ throw new Error(name ? `${name} is not in the library` : "Usage: skilllib show <skill-name>");
3165
3188
  }
3166
3189
  const { description } = readSkillInfo(dir);
3167
3190
  if (args.json) {
@@ -3187,8 +3210,7 @@ function harnessesCommand(args) {
3187
3210
  if (args.positional.length) {
3188
3211
  const unknown = args.positional.filter((p) => !ids.includes(p));
3189
3212
  if (unknown.length) {
3190
- error(`Unknown harness: ${unknown.join(", ")}. Choose from: ${ids.join(", ")}`);
3191
- process.exit(1);
3213
+ throw new Error(`Unknown harness: ${unknown.join(", ")}. Choose from: ${ids.join(", ")}`);
3192
3214
  }
3193
3215
  setHarnesses(args.positional);
3194
3216
  success(`Using ${args.positional.join(", ")}`);
@@ -3225,14 +3247,12 @@ function globalCommand(args) {
3225
3247
  if (action === "keep" || action === "unkeep") {
3226
3248
  const unknown = names.filter((n) => !yours.some((m) => m.name === n));
3227
3249
  if (!names.length || unknown.length) {
3228
- error(names.length ? `Not one of your global skills: ${unknown.join(", ")}` : `Usage: skilllib global ${action} <name>...`);
3229
- process.exit(1);
3250
+ throw new Error(names.length ? `Not one of your global skills: ${unknown.join(", ")}` : `Usage: skilllib global ${action} <name>...`);
3230
3251
  }
3231
3252
  setKeepGlobal(names, action === "keep");
3232
3253
  success(action === "keep" ? `Keeping ${names.join(", ")} global on purpose` : `${names.join(", ")} will show up in cleanup again`);
3233
3254
  } else if (action) {
3234
- error("Usage: skilllib global [keep|unkeep <name>...] | skilllib global move <name>... --to <repo>...");
3235
- process.exit(1);
3255
+ throw new Error("Usage: skilllib global [keep|unkeep <name>...] | skilllib global move <name>... --to <repo>...");
3236
3256
  }
3237
3257
  const kept = keptGlobal();
3238
3258
  if (!yours.length) return info("None of your skills load globally.");
@@ -3285,8 +3305,7 @@ function tidy(args) {
3285
3305
  const names = args.positional;
3286
3306
  const keepFor = (base) => args.keep ? Object.fromEntries(names.map((n) => [n, base ? resolve7(base, args.keep) : expandHome(args.keep)])) : {};
3287
3307
  if (args.keep && !names.length) {
3288
- error("Usage: skilllib tidy <name>... --keep <folder>");
3289
- process.exit(1);
3308
+ throw new Error("Usage: skilllib tidy <name>... --keep <folder>");
3290
3309
  }
3291
3310
  const targets = args.global ? [null] : args.all ? visibleProjects() : [requireProject()];
3292
3311
  const problems = [];
@@ -3306,8 +3325,7 @@ function tidy(args) {
3306
3325
  }
3307
3326
  }
3308
3327
  if (problems.length) {
3309
- for (const p of problems) error(p);
3310
- process.exit(1);
3328
+ throw new Error(problems.join("\n"));
3311
3329
  }
3312
3330
  const reports = targets.map(
3313
3331
  (root) => root === null ? { label: "global folders", report: planGlobalTidy({ keep: keepFor(null) }) } : { label: basename9(root), report: planProjectTidy(root, { keep: keepFor(root) }) }
@@ -3347,8 +3365,7 @@ function agentSkill(args) {
3347
3365
  if (action === "install") {
3348
3366
  const r = installAgentSkill();
3349
3367
  if (!r.ok) {
3350
- error(`Can't install the skilllib skill: ${r.reason}`);
3351
- process.exit(1);
3368
+ throw new Error(`Can't install the skilllib skill: ${r.reason}`);
3352
3369
  }
3353
3370
  if (args.json) return json("agent-skill", { state: agentSkillState(), dirs: r.dirs });
3354
3371
  success(`Installed in ${r.dirs.map((d) => tildify(d)).join(" and ")}`);
@@ -3380,6 +3397,8 @@ export {
3380
3397
  projectHere,
3381
3398
  readManifest,
3382
3399
  setJsonMode,
3400
+ isJsonMode,
3401
+ json,
3383
3402
  error,
3384
3403
  tildify,
3385
3404
  readConfig,
@@ -3406,7 +3425,7 @@ export {
3406
3425
  relinkDependency,
3407
3426
  addSkill,
3408
3427
  removeSkill,
3409
- syncProject,
3428
+ syncSkill,
3410
3429
  updateProject,
3411
3430
  importSkill,
3412
3431
  removeUntracked,
package/dist/index.js CHANGED
@@ -9,6 +9,8 @@ import {
9
9
  globalCommand,
10
10
  harnessesCommand,
11
11
  importSkills,
12
+ isJsonMode,
13
+ json,
12
14
  link,
13
15
  list,
14
16
  newSkill,
@@ -28,7 +30,7 @@ import {
28
30
  unhide,
29
31
  update,
30
32
  usage
31
- } from "./chunk-LO4FMXDE.js";
33
+ } from "./chunk-OFWHWIER.js";
32
34
 
33
35
  // src/index.ts
34
36
  import { readFileSync } from "fs";
@@ -91,15 +93,15 @@ function packageVersion() {
91
93
  }
92
94
  async function main() {
93
95
  const [command, ...rest] = process.argv.slice(2);
96
+ setJsonMode(rest.includes("--json"));
94
97
  const args = parseArgs(rest);
95
- setJsonMode(args.json);
96
98
  if (!["--version", "-v", "version", "help", "--help", "-h"].includes(command ?? "")) refreshAgentSkill();
97
99
  switch (command) {
98
100
  case void 0:
99
- return process.stdin.isTTY && process.stdout.isTTY ? (await import("./tui-3LDZ5GOZ.js")).tui() : overview(args);
101
+ return process.stdin.isTTY && process.stdout.isTTY ? (await import("./tui-NGTOOVAQ.js")).tui() : overview(args);
100
102
  case "ui":
101
103
  if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("`skilllib ui` needs an interactive terminal. Agents and scripts: use `skilllib status --json`.");
102
- return (await import("./tui-3LDZ5GOZ.js")).tui();
104
+ return (await import("./tui-NGTOOVAQ.js")).tui();
103
105
  case "status":
104
106
  return status(args);
105
107
  case "add":
@@ -108,7 +110,6 @@ async function main() {
108
110
  case "rm":
109
111
  return remove(args);
110
112
  case "sync":
111
- case "install":
112
113
  return sync(args);
113
114
  case "update":
114
115
  case "upgrade":
@@ -165,6 +166,7 @@ async function main() {
165
166
  console.log(HELP);
166
167
  return;
167
168
  default:
169
+ if (isJsonMode()) throw new Error(`Unknown command: ${command} (skilllib help lists them)`);
168
170
  console.error(`Unknown command: ${command}
169
171
  `);
170
172
  console.log(HELP);
@@ -172,7 +174,9 @@ async function main() {
172
174
  }
173
175
  }
174
176
  main().catch((err) => {
175
- error(err instanceof Error ? err.message : String(err));
177
+ const message = err instanceof Error ? err.message : String(err);
178
+ if (isJsonMode()) json("error", { error: message });
179
+ error(message);
176
180
  if (process.env.DEBUG && err instanceof Error) console.error(err.stack);
177
181
  process.exit(1);
178
182
  });
package/dist/json.d.cts CHANGED
@@ -178,6 +178,10 @@ type DoctorJson = {
178
178
  message: string;
179
179
  };
180
180
  }[];
181
+ /** Any command with --json that fails: it exits 1, and this is the one line on stdout. */
182
+ type ErrorJson = {
183
+ error: string;
184
+ };
181
185
  /** `skilllib apply <issue-id> [choice] --json` */
182
186
  type ApplyJson = {
183
187
  id: string;
@@ -219,4 +223,4 @@ type AgentSkillJson = {
219
223
  removed?: string[];
220
224
  };
221
225
 
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 };
226
+ export type { AgentSkillJson, AgentSkillState, AllChangesJson, ApplyJson, ChangeJson, ChangesJson, DoctorJson, ErrorJson, GitState, GlobalMoveJson, HarnessId, ImportJson, ListJson, OutdatedJson, ProjectSource, ProjectsJson, ShowJson, SkillState, SourceKind, StatusGlobalGroup, StatusIssue, StatusJson, StatusSkillGroup, UsageJson };
package/dist/json.d.ts CHANGED
@@ -178,6 +178,10 @@ type DoctorJson = {
178
178
  message: string;
179
179
  };
180
180
  }[];
181
+ /** Any command with --json that fails: it exits 1, and this is the one line on stdout. */
182
+ type ErrorJson = {
183
+ error: string;
184
+ };
181
185
  /** `skilllib apply <issue-id> [choice] --json` */
182
186
  type ApplyJson = {
183
187
  id: string;
@@ -219,4 +223,4 @@ type AgentSkillJson = {
219
223
  removed?: string[];
220
224
  };
221
225
 
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 };
226
+ export type { AgentSkillJson, AgentSkillState, AllChangesJson, ApplyJson, ChangeJson, ChangesJson, DoctorJson, ErrorJson, GitState, GlobalMoveJson, HarnessId, ImportJson, ListJson, OutdatedJson, ProjectSource, ProjectsJson, ShowJson, SkillState, SourceKind, StatusGlobalGroup, StatusIssue, StatusJson, StatusSkillGroup, UsageJson };
@@ -63,7 +63,7 @@ import {
63
63
  setPluginOn,
64
64
  skilllibHome,
65
65
  skillsLoadedIn,
66
- syncProject,
66
+ syncSkill,
67
67
  tildify,
68
68
  turnOffIn,
69
69
  uninstallPlugin,
@@ -73,7 +73,7 @@ import {
73
73
  versionHistory,
74
74
  visibilityOf,
75
75
  visibleProjects
76
- } from "./chunk-LO4FMXDE.js";
76
+ } from "./chunk-OFWHWIER.js";
77
77
 
78
78
  // src/tui/index.tsx
79
79
  import { render } from "ink";
@@ -1671,7 +1671,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1671
1671
  pickRepos: (w, repos) => w.ops.moveGlobal(mine, repos, label)
1672
1672
  }),
1673
1673
  ...mine.some((m) => !m.kept) ? [fixOption({ label: `Keep all ${mine.length} global on purpose`, preview: `skilllib stops warning about these ${n(mine.length)}.`, run: (w) => (mine.forEach((m) => w.ops.keepGlobal(m.name, true)), `${n(mine.length)} marked as global on purpose`) })] : [],
1674
- fixOption({ label: `Delete all ${mine.length}`, preview: `Move these ${n(mine.length)} to Settings \u203A Backups.`, run: (w) => (mine.forEach((m) => w.ops.deleteGlobal(m)), `Deleted ${n(mine.length)} (in Settings \u203A Backups)`) })
1674
+ fixOption({ label: `Delete all ${mine.length}`, preview: `Move these ${n(mine.length)} to Settings \u203A Backups.`, run: (w) => joined(mine.map((m) => w.ops.deleteGlobal(m))) })
1675
1675
  ] : [],
1676
1676
  ...libs.length ? [
1677
1677
  fixOption({
@@ -1683,7 +1683,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1683
1683
  })
1684
1684
  ] : [],
1685
1685
  ...libs.length ? [fixOption(deleteLibraryFix(world, libs.map((l) => l.name)))] : [],
1686
- ...locals.length ? [fixOption({ label: `Remove all ${locals.length} from this repo`, preview: `Remove these ${n(locals.length)} from ${open}; your library keeps the tracked ones.`, run: (w) => (locals.forEach((s) => w.ops.remove(open, s.name)), `Removed ${n(locals.length)} from ${open}`) })] : [],
1686
+ ...locals.length ? [fixOption({ label: `Remove all ${locals.length} from this repo`, preview: `Remove these ${n(locals.length)} from ${open}; your library keeps the tracked ones.`, run: (w) => joined(locals.map((s) => w.ops.remove(open, s.name))) })] : [],
1687
1687
  ...[...new Set(machines.filter((m) => m.source === "plugin").map((m) => m.where))].map((id) => ({ label: `Open ${id.split("@")[0]} in Plugins`, action: () => openInPlugins(id) })),
1688
1688
  { label: "Review prompt", action: () => ui.copy(reviewPrompt(world, names, `Review these related skills (${label}).`), `a review prompt for ${label}`) }
1689
1689
  ];
@@ -2013,7 +2013,8 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
2013
2013
  if (row.create && !/^[a-z0-9]/.test(m.query)) return setToast("Type the new skill's name first");
2014
2014
  setModal(null);
2015
2015
  if (!row.create) return apply(row.run);
2016
- row.run(world);
2016
+ const created = row.run(world);
2017
+ if (isFailure(created)) return setToast(`\u2717 ${said2(created)}`);
2017
2018
  return edit(world.ops.libraryFile(m.query), row.afterEdit);
2018
2019
  }
2019
2020
  if (isNameChar(input, key)) return setModal({ ...m, query: m.query + input.toLowerCase(), cursor: 0 });
@@ -2636,7 +2637,7 @@ function realOps(roots, backups) {
2636
2637
  return orForce(removeSkill(rootOf(repo), name), repo, "Remove it anyway", () => removeSkill(rootOf(repo), name, { force: true }));
2637
2638
  },
2638
2639
  restore: (repo, name) => {
2639
- const c = syncProject(rootOf(repo)).find((x) => x.name === name);
2640
+ const [c] = syncSkill(rootOf(repo), name);
2640
2641
  return c ? result(c, repo) : `${name} is already in place`;
2641
2642
  },
2642
2643
  update: (repo, name) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skilllib",
3
- "version": "1.11.0",
3
+ "version": "1.11.2",
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",
@@ -8,7 +8,7 @@ allowed-tools: Bash(skilllib:*)
8
8
 
9
9
  The user manages agent skills with the `skilllib` CLI. Their **library** (`~/.skilllib/library`, "my skills", "the registry") holds versioned skills that load nowhere until added to a repo. A repo pins its skills in `skilllib.json`.
10
10
 
11
- Always pass `--json` (one line of JSON on stdout; messages go to stderr). Never run bare `skilllib` or `skilllib ui`: that's an interactive app.
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` and `global keep` print messages only. Unknown flags are errors. Never run bare `skilllib` or `skilllib ui`: that's an interactive app.
12
12
 
13
13
  ## Reading
14
14
 
@@ -25,12 +25,12 @@ Also: `usage --json` (Claude Code usage only, so "unused" is a hint; each skill
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. 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.
28
+ `add <name>...` · `remove <name>...` · `update [name...]` · `sync` (installs what `skilllib.json` pins; takes no names). 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. `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
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
- - Changes apply from the agent's next session. Remind the user to commit `skilllib.json` and `.claude/skills`.
33
+ - Changes apply from the agent's next session. Remind the user to commit `skilllib.json` and the folders in `dirs`.
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
36