skilllib 1.12.1 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import {
2
+ FEATURED_HARNESSES,
2
3
  HARNESSES,
3
4
  PROJECT_SKILLS_DIR,
4
5
  addRefusal,
@@ -43,6 +44,8 @@ import {
43
44
  moveGlobalSkills,
44
45
  movedTogether,
45
46
  notWritable,
47
+ personalPinsFile,
48
+ pinsIn,
46
49
  pluginBackups,
47
50
  projectHere,
48
51
  projectOfFactory,
@@ -81,10 +84,11 @@ import {
81
84
  updatePlugin,
82
85
  updateProject,
83
86
  usageIssues,
87
+ versionForHash,
84
88
  versionHistory,
85
89
  visibleProjects,
86
90
  withoutForceHint
87
- } from "./chunk-LORSEDNW.js";
91
+ } from "./chunk-C6TSR7NT.js";
88
92
 
89
93
  // src/tui/index.tsx
90
94
  import { render } from "ink";
@@ -445,7 +449,7 @@ function globalFixes(m) {
445
449
  ];
446
450
  }
447
451
  function pinFix(w, repo, s) {
448
- if (s.source !== "lib" || !w.library.some((l) => l.name === s.name)) return null;
452
+ if (s.source !== "lib" || !s.pinMoves || !w.library.some((l) => l.name === s.name)) return null;
449
453
  if (s.personal)
450
454
  return { label: "Pin it for the team", preview: `Move ${s.name}'s pin into ${repo}'s skilllib.json. Git will see skilllib.json and ${s.dir}/${s.name}: commit them to share it.`, run: (w2) => w2.ops.pin(repo, s.name, false) };
451
455
  if (s.inGit || isShared(s.git)) return null;
@@ -1506,8 +1510,11 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1506
1510
  }
1507
1511
  useEffect(() => {
1508
1512
  const folder = () => !initial.roots.length && askForFolder("Where are your projects? skilllib finds the git repos in there");
1509
- if (!initial.agentsChosen) setModal({ kind: "agents", selected: initial.agents, cursor: 0, then: folder });
1510
- else folder();
1513
+ if (!initial.agentsChosen) {
1514
+ const installed = HARNESSES.filter((h) => h.installed()).map((h) => h.id);
1515
+ const ids = [...FEATURED_HARNESSES, ...installed.filter((id) => !FEATURED_HARNESSES.includes(id))];
1516
+ setModal({ kind: "agents", selected: initial.agents, ids, cursor: 0, then: folder });
1517
+ } else folder();
1511
1518
  }, []);
1512
1519
  function openInGlobal(name) {
1513
1520
  goPlace("global");
@@ -1527,14 +1534,24 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1527
1534
  cells: [{ text, width: 34, color: c }, { text: sub, grow: true, color: color.faint }],
1528
1535
  onEnter
1529
1536
  });
1537
+ const shown = [
1538
+ ...FEATURED_HARNESSES.map((id) => harness(id)),
1539
+ ...world.agents.filter((id) => !FEATURED_HARNESSES.includes(id)).map((id) => harness(id))
1540
+ ];
1530
1541
  return [
1531
1542
  header("#agents", "Agents"),
1532
- ...HARNESSES.map(
1543
+ ...shown.map(
1533
1544
  (h) => row(`agent:${h.id}`, `${world.agents.includes(h.id) ? "[x]" : "[ ]"} ${h.icon} ${h.name}`, h.projectDirs.join(", "), () => {
1534
1545
  const wasOn = world.agents.includes(h.id);
1535
1546
  apply((w) => (w.ops.setAgents(wasOn ? w.agents.filter((a) => a !== h.id) : HARNESSES.map((x) => x.id).filter((id) => id === h.id || w.agents.includes(id))), `${h.name} ${wasOn ? "off" : "on"}`));
1536
1547
  })
1537
1548
  ),
1549
+ row(
1550
+ "agents:more",
1551
+ "More agents\u2026",
1552
+ "Copilot, Gemini, OpenCode, Windsurf, and the rest",
1553
+ () => setModal({ kind: "agents", selected: world.agents, ids: HARNESSES.map((h) => h.id), cursor: 0 })
1554
+ ),
1538
1555
  // The skill that lets your agents use skilllib: installing adds a global skill, so it asks first.
1539
1556
  row(
1540
1557
  "agent-skill",
@@ -1664,11 +1681,13 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1664
1681
  if (m.kind === "add") return addKeys(m, input, key);
1665
1682
  }
1666
1683
  function agentsKeys(m, input, key) {
1684
+ const list2 = m.ids.map((id) => harness(id));
1667
1685
  if (key.escape && !m.then) return setModal(null);
1668
1686
  if (key.upArrow) return setModal({ ...m, cursor: Math.max(0, m.cursor - 1) });
1669
- if (key.downArrow) return setModal({ ...m, cursor: Math.min(HARNESSES.length - 1, m.cursor + 1) });
1687
+ if (key.downArrow) return setModal({ ...m, cursor: Math.min(list2.length - 1, m.cursor + 1) });
1670
1688
  if (input === " ") {
1671
- const id = HARNESSES[m.cursor].id;
1689
+ const id = list2[m.cursor]?.id;
1690
+ if (!id) return;
1672
1691
  return setModal({ ...m, selected: m.selected.includes(id) ? m.selected.filter((x) => x !== id) : [...m.selected, id] });
1673
1692
  }
1674
1693
  if (key.return) {
@@ -1902,11 +1921,11 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1902
1921
  empty: "",
1903
1922
  header: /* @__PURE__ */ jsx2(Text2, { color: color.muted, children: "skilllib makes your skills visible to each one. Detected ones are ticked." }),
1904
1923
  headerHeight: 1,
1905
- rows: HARNESSES.map((h) => ({
1924
+ rows: modal.ids.map((id) => harness(id)).map((h) => ({
1906
1925
  key: h.id,
1907
1926
  cells: [
1908
1927
  { text: modal.selected.includes(h.id) ? "[x] " : "[ ] ", width: 4, color: modal.selected.includes(h.id) ? color.green : color.faint },
1909
- { text: `${h.icon} ${h.name}`, width: 18, color: h.color },
1928
+ { text: `${h.icon} ${h.name}`, width: 22, color: h.color },
1910
1929
  { text: `reads ${h.projectDirs.join(", ")}`, grow: true, color: color.faint }
1911
1930
  ]
1912
1931
  }))
@@ -2095,7 +2114,7 @@ function uniqueNames(roots) {
2095
2114
  })
2096
2115
  );
2097
2116
  }
2098
- function localSkill(root, s, git) {
2117
+ function localSkill(root, s, git, pins, canBePersonal) {
2099
2118
  const managed = {
2100
2119
  "edited locally": { edited: true },
2101
2120
  "edited locally, update available": { edited: true },
@@ -2114,7 +2133,9 @@ function localSkill(root, s, git) {
2114
2133
  const state = git ? git.of(relativeTo(root, s.path)) : null;
2115
2134
  const others = otherCopies(root, s.name, s.location);
2116
2135
  const own = other[s.state]?.source === "untracked" && isShared(state) ? { source: "repo" } : {};
2117
- const inGit = s.managed && (isShared(state) || (readManifest(root).skills[s.name]?.links ?? []).some((d) => isShared(git?.of(`${d}/${s.name}`))));
2136
+ const dep = pins[s.name];
2137
+ const inGit = s.managed && (isShared(state) || (dep?.links ?? []).some((d) => isShared(git?.of(`${d}/${s.name}`))));
2138
+ const pinMoves = s.managed && !!dep && versionForHash(s.name, dep.hash) !== null && !s.state.startsWith("edited") && (s.personal || canBePersonal);
2118
2139
  return {
2119
2140
  name: s.name,
2120
2141
  source: "lib",
@@ -2122,7 +2143,7 @@ function localSkill(root, s, git) {
2122
2143
  path: s.path,
2123
2144
  // Agents that actually load it here: visibility lists every agent you use, with `paths: 0` for the blind ones.
2124
2145
  agents: s.visibility.filter((v) => v.paths > 0).map((v) => v.id),
2125
- ...s.managed ? { version: s.version ?? void 0, ...s.personal ? { personal: true } : {}, ...inGit ? { inGit: true } : {}, ...managed[s.state] } : { ...other[s.state], ...own },
2146
+ ...s.managed ? { version: s.version ?? void 0, ...s.personal ? { personal: true } : {}, ...inGit ? { inGit: true } : {}, ...pinMoves ? { pinMoves: true } : {}, ...managed[s.state] } : { ...other[s.state], ...own },
2126
2147
  ...s.source ? { origin: `npx skills: ${s.source}` } : {},
2127
2148
  ...others.length ? { others } : {},
2128
2149
  git: state
@@ -2219,11 +2240,11 @@ function loadWorld() {
2219
2240
  name: names.get(root),
2220
2241
  path: root,
2221
2242
  manifest: hasManifest ? git ? git.of("skilllib.json") : "no git" : "none",
2222
- skills: status.map((s) => {
2223
- const local = localSkill(root, s, git);
2243
+ skills: ((pins, canBePersonal) => status.map((s) => {
2244
+ const local = localSkill(root, s, git, pins, canBePersonal);
2224
2245
  const own = s.state.includes("missing") ? "" : readSkillInfo(s.path).description;
2225
2246
  return own && own !== descriptions[s.name] ? { ...local, description: own } : local;
2226
- }),
2247
+ }))(pinsIn(root), personalPinsFile(root) !== null),
2227
2248
  ...loadedHere(root)
2228
2249
  };
2229
2250
  });
@@ -2376,8 +2397,14 @@ function realOps(roots, backups, machine, libraryNames) {
2376
2397
  },
2377
2398
  pin: (repo, name, personal) => {
2378
2399
  const c = addSkill(rootOf(repo), name, { personal });
2379
- if (c.action === "skipped") return failed(said3(c, repo));
2380
- return personal ? `${name} is now personal in ${repo}: only on your machines, kept out of git` : `${name} is now pinned in ${repo}'s skilllib.json for the team`;
2400
+ if (c.action === "skipped") return failed(`${name}: ${withoutForceHint(c.reason ?? "skipped")}`);
2401
+ const moved = personal ? `${name} is now personal in ${repo}: only on your machines, kept out of git` : `${name} is now pinned in ${repo}'s skilllib.json for the team`;
2402
+ if (c.uncommitted?.length) {
2403
+ const copy = `${copyDir(repo, name)}/${name}`;
2404
+ return failed(`${moved}, but not linked in ${c.uncommitted.join(", ")}: git tracks ${c.uncommitted.length === 1 ? "it" : "them"} and ${copy} isn't committed, so agents reading ${c.uncommitted.length === 1 ? "it" : "them"} can't use ${name}. Commit ${copy}, then run skilllib link --allow-tracked`);
2405
+ }
2406
+ if (c.blocked?.length || c.inTheWay?.length) return withLinks(c, repo, name);
2407
+ return moved;
2381
2408
  },
2382
2409
  copyToLibrary: (repo, name) => ((s) => s ? toLibrary(s.path) : failed(`${name} isn't in ${repo} any more`))(find(repo, name)),
2383
2410
  deleteGlobal: (m) => {
package/package.json CHANGED
@@ -1,15 +1,19 @@
1
1
  {
2
2
  "name": "skilllib",
3
- "version": "1.12.1",
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.",
3
+ "version": "1.13.0",
4
+ "description": "Manage your agent skills like dependencies. One library, install from skills.sh, and every agent you use can see them.",
5
5
  "keywords": [
6
6
  "agent-skills",
7
7
  "skills",
8
+ "skills.sh",
8
9
  "skill-md",
9
10
  "claude-code",
10
11
  "claude",
11
12
  "cursor",
12
13
  "codex",
14
+ "gemini",
15
+ "github-copilot",
16
+ "opencode",
13
17
  "zed",
14
18
  "grok",
15
19
  "ai-agents",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: skilllib
3
- description: Agent skills (SKILL.md) loaded in this repo and the user's skill library, via the skilllib CLI. Use for which skills you can use here and where each comes from, which library skills to add, what's out of date, broken or duplicated, and adding, removing or updating skills.
3
+ description: Agent skills (SKILL.md) loaded in this repo and the user's skill library, via the skilllib CLI. Use for which skills you can use here and where each comes from, which library skills to add, what's out of date, broken or duplicated, adding, removing or updating skills, and installing skills from skills.sh or GitHub.
4
4
  allowed-tools: Bash(skilllib:*)
5
5
  ---
6
6
 
@@ -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` (shared, committed), or in the user's personal pins (`~/.skilllib/pins`, kept out of git: `personal: true` in the output).
10
10
 
11
- Always pass `--json` (one line of JSON on stdout; messages go to stderr). A failure exits 1 with `{"error": "..."}` as that line. `link`, `tidy`, `global` (its listing, `keep` and `unkeep`), `restore`, `new`, `folders`, `harnesses`, `scan` and `unhide` print messages only (read the exit code). Unknown flags are errors. Never run bare `skilllib` or `skilllib ui`: that's an interactive app.
11
+ Always pass `--json` (one line of JSON on stdout; messages go to stderr). A failure exits 1 with `{"error": "..."}` as that line. `link`, `tidy`, `global` (its listing, `keep` and `unkeep`), `restore`, `new`, `folders`, `harnesses`, `scan`, `search` (without `--json`) and `unhide` print messages only (read the exit code). Unknown flags are errors. Never run bare `skilllib` or `skilllib ui`: that's an interactive app.
12
12
 
13
13
  ## Reading
14
14
 
@@ -25,7 +25,7 @@ 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>...` (pins in `skilllib.json` when the repo has one, else in personal pins; `--personal` / `--shared` choose, and move an existing pin without changing its version) · `remove <name>...` (skills skilllib manages, `source: library`) · `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, inTheWay, 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}`.
28
+ `add <name>...` (pins in `skilllib.json` when the repo has one, else in personal pins; `--personal` / `--shared` choose, and move an existing pin without changing its version) · `remove <name>...` (skills skilllib manages, `source: library`) · `update [name...]` · `sync` (installs what's pinned, in `skilllib.json` and your personal pins; takes no names). Pass several names in one call. Their result is `[{name, action, from, to, dirs, personal, moved, reason, blocked, uncommitted, inTheWay, backedUp}]` (`personal: true` when the pin is in your personal pins; `moved` when `--personal`/`--shared` moved it), which is the whole result: there's no need to check files afterwards. `import <dir>...` (into the library) prints `[{path, name, status}]` and exits 1 if a folder was skipped (`exists`, `no SKILL.md`). `doctor --fix` prints doctor's issues, each fix with `fixed: {ok, message}`.
29
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
  - `removed` + `reason`: removed from skilllib.json, but something was left as it is (its copy behind a folder that links out of the repo, a file of the user's, or a link in such a folder). Tell the user what the reason says is still there.
@@ -42,7 +42,7 @@ Also: `usage --json` (Claude Code usage only, so "unused" is a hint; each skill
42
42
 
43
43
  **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.
44
44
 
45
- **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.
45
+ **Importing and installing.** `import <dir>...` copies skill folders (each with a `SKILL.md`) into the library; it doesn't download anything. `install <source>...` does: `owner/repo`, `owner/repo@skill`, a `https://skills.sh/...` or GitHub URL, or a local folder. It puts each skill in the library. In a repo it also adds them here; `--global` loads them in every repo instead (and marks them kept). A repo with several skills is listed, not installed, until you pass `@name` or `--all`. `search <query>` lists matches from skills.sh. `install` and `search` accept `--json`.
46
46
 
47
47
  ## Now
48
48