skilllib 1.11.7 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -31,7 +31,7 @@ import {
31
31
  unhide,
32
32
  update,
33
33
  usage
34
- } from "./chunk-PKQPUIN3.js";
34
+ } from "./chunk-57EEPVXJ.js";
35
35
 
36
36
  // src/index.ts
37
37
  import { readFileSync } from "fs";
@@ -53,6 +53,8 @@ Set up:
53
53
  In a project (a git repo or a folder with skilllib.json; never your home folder):
54
54
  skilllib status This project's skills, which agents load them, and issues
55
55
  skilllib add <name>... Install library skills where your harnesses look
56
+ (pinned in skilllib.json when the repo has one, else in your
57
+ personal pins, kept out of git; --personal / --shared to choose)
56
58
  skilllib remove <name>... Remove skills from this project
57
59
  skilllib link [--all] Make every skill here usable by all your agents (adds links)
58
60
  skilllib tidy [--all|--global] One real copy per skill, plus only the links your agents need
@@ -96,7 +98,7 @@ var FLAGS_OF = {
96
98
  "": ["days"],
97
99
  ui: [],
98
100
  status: ["days"],
99
- add: ["force", "allow-tracked"],
101
+ add: ["force", "allow-tracked", "personal", "shared"],
100
102
  remove: ["force"],
101
103
  sync: ["all", "force"],
102
104
  update: ["all", "force", "allow-tracked"],
@@ -138,10 +140,10 @@ async function main() {
138
140
  if (!["--version", "-v", "version", "help", "--help", "-h"].includes(command ?? "")) refreshAgentSkill();
139
141
  switch (command) {
140
142
  case void 0:
141
- return process.stdin.isTTY && process.stdout.isTTY ? (await import("./tui-B5PF3J3E.js")).tui() : overview(args);
143
+ return process.stdin.isTTY && process.stdout.isTTY ? (await import("./tui-BCCAP7GJ.js")).tui() : overview(args);
142
144
  case "ui":
143
145
  if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("`skilllib ui` needs an interactive terminal. Agents and scripts: use `skilllib status --json`.");
144
- return (await import("./tui-B5PF3J3E.js")).tui();
146
+ return (await import("./tui-BCCAP7GJ.js")).tui();
145
147
  case "status":
146
148
  return status(args);
147
149
  case "add":
package/dist/json.d.cts CHANGED
@@ -33,6 +33,8 @@ type Change = {
33
33
  inTheWay?: string[];
34
34
  /** Where overwritten local edits went (~/.skilllib/edit-backup/…); `skilllib restore` puts them back. */
35
35
  backedUp?: string;
36
+ /** The pin moved: to your personal pins, or to the repo's skilllib.json. */
37
+ moved?: "personal" | "shared";
36
38
  };
37
39
 
38
40
  /**
@@ -67,6 +69,8 @@ type StatusSkillGroup = {
67
69
  git?: GitState;
68
70
  /** Only when some of your agents don't load it here. */
69
71
  agents?: HarnessId[];
72
+ /** In your personal pins (~/.skilllib/pins): only on your machines, kept out of git. */
73
+ personal?: true;
70
74
  };
71
75
  /** Global skills that come from the same place and load in the same agents. */
72
76
  type StatusGlobalGroup = {
@@ -145,6 +149,8 @@ type ProjectsJson = {
145
149
  type ChangeJson = Change & {
146
150
  /** Where an installed, updated or reset skill now is: the real copy first, then its links. */
147
151
  dirs?: string[];
152
+ /** Pinned in your personal pins, kept out of git: nothing to commit. */
153
+ personal?: true;
148
154
  };
149
155
  /** `skilllib add|remove --json`, and `sync|update --json` without --all. */
150
156
  type ChangesJson = ChangeJson[];
package/dist/json.d.ts CHANGED
@@ -33,6 +33,8 @@ type Change = {
33
33
  inTheWay?: string[];
34
34
  /** Where overwritten local edits went (~/.skilllib/edit-backup/…); `skilllib restore` puts them back. */
35
35
  backedUp?: string;
36
+ /** The pin moved: to your personal pins, or to the repo's skilllib.json. */
37
+ moved?: "personal" | "shared";
36
38
  };
37
39
 
38
40
  /**
@@ -67,6 +69,8 @@ type StatusSkillGroup = {
67
69
  git?: GitState;
68
70
  /** Only when some of your agents don't load it here. */
69
71
  agents?: HarnessId[];
72
+ /** In your personal pins (~/.skilllib/pins): only on your machines, kept out of git. */
73
+ personal?: true;
70
74
  };
71
75
  /** Global skills that come from the same place and load in the same agents. */
72
76
  type StatusGlobalGroup = {
@@ -145,6 +149,8 @@ type ProjectsJson = {
145
149
  type ChangeJson = Change & {
146
150
  /** Where an installed, updated or reset skill now is: the real copy first, then its links. */
147
151
  dirs?: string[];
152
+ /** Pinned in your personal pins, kept out of git: nothing to commit. */
153
+ personal?: true;
148
154
  };
149
155
  /** `skilllib add|remove --json`, and `sync|update --json` without --all. */
150
156
  type ChangesJson = ChangeJson[];
@@ -84,7 +84,7 @@ import {
84
84
  versionHistory,
85
85
  visibleProjects,
86
86
  withoutForceHint
87
- } from "./chunk-PKQPUIN3.js";
87
+ } from "./chunk-57EEPVXJ.js";
88
88
 
89
89
  // src/tui/index.tsx
90
90
  import { render } from "ink";
@@ -347,6 +347,7 @@ function shortOf(found) {
347
347
  unavailable: "Pinned, not in your library",
348
348
  pin: "Pinned version not in your library",
349
349
  "out-of-sync": "Not the pinned version",
350
+ shadowed: "Pinned twice",
350
351
  update: "Out of date",
351
352
  uncommitted: "Commit it first",
352
353
  local: "Not in your library",
@@ -443,6 +444,17 @@ function globalFixes(m) {
443
444
  { label: "Delete it", preview: `Remove ${m.where}/${m.name}. A backup is kept in Settings \u203A Backups.`, run: (w) => w.ops.deleteGlobal(m) }
444
445
  ];
445
446
  }
447
+ function pinFix(w, repo, s) {
448
+ if (s.source !== "lib" || !w.library.some((l) => l.name === s.name)) return null;
449
+ if (s.personal)
450
+ 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
+ if (s.inGit || isShared(s.git)) return null;
452
+ return {
453
+ label: "Make it personal",
454
+ preview: `Move ${s.name}'s pin out of ${repo}'s skilllib.json into your personal pins, and keep its copy out of git.${project(w, repo).manifest === "committed" ? " Git will see skilllib.json change: once that's committed, teammates no longer get it." : ""}`,
455
+ run: (w2) => w2.ops.pin(repo, s.name, true)
456
+ };
457
+ }
446
458
  function machineActions(m) {
447
459
  if (m.broken) return [];
448
460
  return m.source === "global" ? globalFixes(m) : m.source === "skilllib" ? [removeAgentSkillFix()] : [];
@@ -759,7 +771,7 @@ var bySeverity = (a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity]
759
771
  function tag(u) {
760
772
  const s = u.local;
761
773
  if (s?.source === "lib") {
762
- return [`lib v${s.version}`, color.accent];
774
+ return [s.personal ? `personal v${s.version}` : `lib v${s.version}`, color.accent];
763
775
  }
764
776
  if (s) return [s.source, color.blue];
765
777
  if (u.source === "global") return [u.machine?.kept ? "global \u2713" : "global", color.yellow];
@@ -951,7 +963,7 @@ function skillActions(w, p, u, ui) {
951
963
  // Its folder gone: all that's left to remove is the skilllib.json entry (the label Health offers too).
952
964
  label: s?.missing ? "Remove it from skilllib.json" : "Remove from repo",
953
965
  preview: (s?.missing ? `${p}'s skilllib.json stops pinning ${u.name}. There's no folder to delete.` : s?.source === "lib" ? `Delete ${s.dir}/${u.name} in ${p}.${inLibrary ? " Your library keeps it." : ""}` : `Move ${s?.dir}/${u.name} in ${p} to Settings \u203A Backups.`) + (s?.others ? ` The copy in ${s.others.join(", ")} stays.` : "") + // A managed skill's removal edits skilllib.json too.
954
- (gitSees(s) || (s?.source === "lib" && project(w, p).manifest === "committed" ? " Git will see skilllib.json change." : "")),
966
+ (gitSees(s) || (s?.source === "lib" && !s.personal && project(w, p).manifest === "committed" ? " Git will see skilllib.json change." : "")),
955
967
  run: (w2) => w2.ops.remove(p, u.name)
956
968
  };
957
969
  if (s?.source === "lib")
@@ -962,6 +974,7 @@ function skillActions(w, p, u, ui) {
962
974
  { label: "Edit in library", action: () => ui.edit(w.ops.libraryFile(u.name)) },
963
975
  versionsOption(w, p, u.name, s.version, ui)
964
976
  ] : [],
977
+ ...((f) => f ? [fixOption(f)] : [])(pinFix(w, p, s)),
965
978
  fixOption(remove),
966
979
  reviewOption(w, u.name, ui, p)
967
980
  ];
@@ -1564,34 +1577,39 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1564
1577
  if (isNameChar(input, key)) return setFocus("list"), setQuery(query + input.toLowerCase());
1565
1578
  if (key.backspace || key.delete) return setQuery(query.slice(0, -1));
1566
1579
  if (input === "?") return setModal({ kind: "help" });
1567
- if (focus === "detail") {
1568
- const lines = (detail?.body ?? []).flatMap((l) => l.row ? [l.row] : l.option !== void 0 ? [[l.option]] : []);
1569
- const at = lines.findIndex((r) => r.includes(detailCursor));
1570
- const col = at < 0 ? 0 : lines[at].indexOf(detailCursor);
1571
- const goLine = (i) => lines[i] && setDetailCursor(lines[i][Math.min(col, lines[i].length - 1)]);
1572
- if (key.escape) return setFocus("list");
1573
- if (key.upArrow) return goLine(Math.max(0, at - 1));
1574
- if (key.downArrow) return goLine(at + 1);
1575
- if (key.leftArrow) return col > 0 ? setDetailCursor(lines[at][col - 1]) : setFocus("list");
1576
- if (key.rightArrow) return lines[at]?.[col + 1] !== void 0 ? setDetailCursor(lines[at][col + 1]) : void 0;
1577
- if ((key.return || input === " ") && detail?.options[detailCursor]) return choose(detail.options[detailCursor]);
1578
- return;
1579
- }
1580
- if (focus === "sidebar") {
1581
- if (key.upArrow || key.downArrow) {
1582
- const d = key.upArrow ? -1 : 1;
1583
- let i = sideAt + d;
1584
- while (sideRows[i]?.header) i += d;
1585
- const next = sideRows[i];
1586
- if (!next) return;
1587
- if (next.key === "help") return setOnHelp(true);
1588
- return next.go(), setOnHelp(false), setFocus("sidebar");
1589
- }
1590
- if (key.return && onHelp) return setModal({ kind: "help" });
1591
- if (key.rightArrow || key.return) return setOnHelp(false), setFocus("list");
1592
- if (key.escape) return setSideQuery("");
1593
- return;
1580
+ if (focus === "detail") return detailKeys(input, key);
1581
+ if (focus === "sidebar") return sidebarKeys(key);
1582
+ return listKeys(input, key);
1583
+ });
1584
+ function detailKeys(input, key) {
1585
+ const lines = (detail?.body ?? []).flatMap((l) => l.row ? [l.row] : l.option !== void 0 ? [[l.option]] : []);
1586
+ const at = lines.findIndex((r) => r.includes(detailCursor));
1587
+ const col = at < 0 ? 0 : lines[at].indexOf(detailCursor);
1588
+ const goLine = (i) => lines[i] && setDetailCursor(lines[i][Math.min(col, lines[i].length - 1)]);
1589
+ if (key.escape) return setFocus("list");
1590
+ if (key.upArrow) return goLine(Math.max(0, at - 1));
1591
+ if (key.downArrow) return goLine(at + 1);
1592
+ if (key.leftArrow) return col > 0 ? setDetailCursor(lines[at][col - 1]) : setFocus("list");
1593
+ if (key.rightArrow) return lines[at]?.[col + 1] !== void 0 ? setDetailCursor(lines[at][col + 1]) : void 0;
1594
+ if ((key.return || input === " ") && detail?.options[detailCursor]) return choose(detail.options[detailCursor]);
1595
+ return;
1596
+ }
1597
+ function sidebarKeys(key) {
1598
+ if (key.upArrow || key.downArrow) {
1599
+ const d = key.upArrow ? -1 : 1;
1600
+ let i = sideAt + d;
1601
+ while (sideRows[i]?.header) i += d;
1602
+ const next = sideRows[i];
1603
+ if (!next) return;
1604
+ if (next.key === "help") return setOnHelp(true);
1605
+ return next.go(), setOnHelp(false), setFocus("sidebar");
1594
1606
  }
1607
+ if (key.return && onHelp) return setModal({ kind: "help" });
1608
+ if (key.rightArrow || key.return) return setOnHelp(false), setFocus("list");
1609
+ if (key.escape) return setSideQuery("");
1610
+ return;
1611
+ }
1612
+ function listKeys(input, key) {
1595
1613
  if (!sidebar && (key.leftArrow || key.rightArrow)) {
1596
1614
  const i = PLACES.findIndex(([p]) => p === place) + (key.leftArrow ? -1 : 1);
1597
1615
  if (PLACES[i]) goPlace(PLACES[i][0]);
@@ -1625,44 +1643,13 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1625
1643
  if (place === "projects" && !open) return key.return ? (setOpen(current.name), setQuery(""), setChip("all")) : void 0;
1626
1644
  if (key.return && detail) return setFocus("detail"), setDetailCursor(0);
1627
1645
  if (input === " ") return fixSelected();
1628
- });
1646
+ }
1629
1647
  function modalKeys(input, key) {
1630
1648
  const m = modal;
1631
1649
  if (m.kind === "help") return setModal(null);
1632
- if (m.kind === "agents") {
1633
- if (key.escape && !m.then) return setModal(null);
1634
- if (key.upArrow) return setModal({ ...m, cursor: Math.max(0, m.cursor - 1) });
1635
- if (key.downArrow) return setModal({ ...m, cursor: Math.min(HARNESSES.length - 1, m.cursor + 1) });
1636
- if (input === " ") {
1637
- const id = HARNESSES[m.cursor].id;
1638
- return setModal({ ...m, selected: m.selected.includes(id) ? m.selected.filter((x) => x !== id) : [...m.selected, id] });
1639
- }
1640
- if (key.return) {
1641
- setModal(null);
1642
- apply((w) => w.ops.setAgents(m.selected));
1643
- m.then?.();
1644
- }
1645
- return;
1646
- }
1647
- if (m.kind === "input") {
1648
- if (key.escape) return setModal(null);
1649
- if (key.return) {
1650
- const value = m.value.trim() || m.placeholder;
1651
- setModal(null);
1652
- return value ? m.submit(value) : void 0;
1653
- }
1654
- if (key.backspace || key.delete) return setModal({ ...m, value: m.value.slice(0, -1) });
1655
- if (key.ctrl && input === "u") return setModal({ ...m, value: "" });
1656
- if (input && !key.ctrl && !key.meta) return setModal({ ...m, value: m.value + input });
1657
- return;
1658
- }
1659
- if (m.kind === "menu") {
1660
- if (key.escape) return setModal(null);
1661
- if (key.upArrow) return setModal({ ...m, cursor: Math.max(0, m.cursor - 1) });
1662
- if (key.downArrow) return setModal({ ...m, cursor: Math.min(m.options.length - 1, m.cursor + 1) });
1663
- if (key.return && m.options[m.cursor]) return setModal(null), choose(m.options[m.cursor]);
1664
- return;
1665
- }
1650
+ if (m.kind === "agents") return agentsKeys(m, input, key);
1651
+ if (m.kind === "input") return inputKeys(m, input, key);
1652
+ if (m.kind === "menu") return menuKeys(m, key);
1666
1653
  if (key.escape) return setModal(null), m.kind === "confirm" && m.toast && setToast(m.toast);
1667
1654
  if (m.kind === "confirm") {
1668
1655
  if (key.return || input === "y") {
@@ -1672,78 +1659,115 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1672
1659
  }
1673
1660
  return;
1674
1661
  }
1675
- if (m.kind === "repos") {
1676
- const repos = (m.only ?? world.projects.map((p) => p.name)).filter((r) => !m.query || matches(r, m.query));
1677
- if (isNameChar(input, key)) return setModal({ ...m, query: m.query + input.toLowerCase(), cursor: 0 });
1678
- if (key.backspace || key.delete) return setModal({ ...m, query: m.query.slice(0, -1), cursor: 0 });
1679
- if (key.upArrow) return setModal({ ...m, cursor: Math.max(0, m.cursor - 1) });
1680
- if (key.downArrow) return setModal({ ...m, cursor: Math.min(repos.length - 1, m.cursor + 1) });
1681
- if (input === " ") {
1682
- const picked = new Set(m.picked);
1683
- const r = repos[m.cursor];
1684
- if (!r) return;
1685
- if (!picked.delete(r)) picked.add(r);
1686
- return setModal({ ...m, picked });
1687
- }
1688
- if (key.return) {
1689
- if (!m.picked.size && !m.fix.allowNone) return setToast("Tick at least one repo with space, or esc to cancel");
1690
- setModal(null);
1691
- setFocus("list");
1692
- apply((w) => m.fix.pickRepos(w, [...m.picked]));
1693
- }
1694
- return;
1662
+ if (m.kind === "repos") return reposKeys(m, input, key);
1663
+ if (m.kind === "fixall") return fixallKeys(m, input, key);
1664
+ if (m.kind === "add") return addKeys(m, input, key);
1665
+ }
1666
+ function agentsKeys(m, input, key) {
1667
+ if (key.escape && !m.then) return setModal(null);
1668
+ 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) });
1670
+ if (input === " ") {
1671
+ const id = HARNESSES[m.cursor].id;
1672
+ return setModal({ ...m, selected: m.selected.includes(id) ? m.selected.filter((x) => x !== id) : [...m.selected, id] });
1695
1673
  }
1696
- if (m.kind === "fixall") {
1697
- if (key.upArrow) return setModal({ ...m, cursor: Math.max(0, m.cursor - 1) });
1698
- if (key.downArrow) return setModal({ ...m, cursor: Math.min(m.items.length - 1, m.cursor + 1) });
1699
- if (input === " ") return setModal({ ...m, items: m.items.map((it, i) => i === m.cursor ? { ...it, on: !it.on } : it) });
1700
- if (key.return) {
1701
- setModal(null);
1702
- const chosen = m.items.filter((it) => it.on);
1703
- apply((w) => runAll(w, chosen.map((it) => it.fix)));
1704
- }
1705
- return;
1674
+ if (key.return) {
1675
+ setModal(null);
1676
+ apply((w) => w.ops.setAgents(m.selected));
1677
+ m.then?.();
1706
1678
  }
1707
- if (m.kind === "add") {
1708
- const rows = addRows(world, m.target, m.query);
1709
- const cursor2 = addCursor(rows, m.cursor);
1710
- const step = (d) => {
1711
- let i = cursor2 + d;
1712
- while (rows[i]?.header) i += d;
1713
- return rows[i] ? setModal({ ...m, cursor: i }) : void 0;
1714
- };
1715
- if (key.upArrow) return step(-1);
1716
- if (key.downArrow) return step(1);
1717
- if (key.backspace || key.delete) return setModal({ ...m, query: m.query.slice(0, -1), cursor: 0 });
1718
- const members = rows[cursor2]?.members;
1719
- if (input === " " && members) {
1720
- const picked = new Set(m.picked);
1721
- const all2 = members.every((n) => picked.has(n));
1722
- for (const n of members) all2 ? picked.delete(n) : picked.add(n);
1723
- return setModal({ ...m, picked });
1724
- }
1725
- if (key.return && m.target && m.picked.size) {
1726
- const [target, names] = [m.target, [...m.picked]];
1727
- setModal(null);
1728
- return apply((w) => w.ops.addTo([target], names));
1729
- }
1730
- if (key.return && rows[cursor2]?.agent) {
1731
- setModal(null);
1732
- return ui.copy(writeSkillPrompt(world, m.query, m.target), "a prompt for an agent to write the skill");
1733
- }
1734
- if (key.return && rows[cursor2]?.run) {
1735
- const row = rows[cursor2];
1736
- if (row.create && !/^[a-z0-9]/.test(m.query)) return setToast("Type the new skill's name first");
1737
- setModal(null);
1738
- if (!row.create) return apply(row.run);
1739
- const created = row.run(world);
1740
- if (isFailure(created)) return setToast(`\u2717 ${said2(created)}`);
1741
- return edit(world.ops.libraryFile(m.query), row.afterEdit);
1742
- }
1743
- if (isNameChar(input, key)) return setModal({ ...m, query: m.query + input.toLowerCase(), cursor: 0 });
1679
+ return;
1680
+ }
1681
+ function inputKeys(m, input, key) {
1682
+ if (key.escape) return setModal(null);
1683
+ if (key.return) {
1684
+ const value = m.value.trim() || m.placeholder;
1685
+ setModal(null);
1686
+ return value ? m.submit(value) : void 0;
1744
1687
  }
1688
+ if (key.backspace || key.delete) return setModal({ ...m, value: m.value.slice(0, -1) });
1689
+ if (key.ctrl && input === "u") return setModal({ ...m, value: "" });
1690
+ if (input && !key.ctrl && !key.meta) return setModal({ ...m, value: m.value + input });
1691
+ return;
1745
1692
  }
1746
- const toastMax = Math.max(1, Math.min(Math.max(3, Math.floor(termRows / 3)), termRows - 8 - (sidebar ? 0 : 1)));
1693
+ function menuKeys(m, key) {
1694
+ if (key.escape) return setModal(null);
1695
+ if (key.upArrow) return setModal({ ...m, cursor: Math.max(0, m.cursor - 1) });
1696
+ if (key.downArrow) return setModal({ ...m, cursor: Math.min(m.options.length - 1, m.cursor + 1) });
1697
+ if (key.return && m.options[m.cursor]) return setModal(null), choose(m.options[m.cursor]);
1698
+ return;
1699
+ }
1700
+ function reposKeys(m, input, key) {
1701
+ const repos = (m.only ?? world.projects.map((p) => p.name)).filter((r) => !m.query || matches(r, m.query));
1702
+ if (isNameChar(input, key)) return setModal({ ...m, query: m.query + input.toLowerCase(), cursor: 0 });
1703
+ if (key.backspace || key.delete) return setModal({ ...m, query: m.query.slice(0, -1), cursor: 0 });
1704
+ if (key.upArrow) return setModal({ ...m, cursor: Math.max(0, m.cursor - 1) });
1705
+ if (key.downArrow) return setModal({ ...m, cursor: Math.min(repos.length - 1, m.cursor + 1) });
1706
+ if (input === " ") {
1707
+ const picked = new Set(m.picked);
1708
+ const r = repos[m.cursor];
1709
+ if (!r) return;
1710
+ if (!picked.delete(r)) picked.add(r);
1711
+ return setModal({ ...m, picked });
1712
+ }
1713
+ if (key.return) {
1714
+ if (!m.picked.size && !m.fix.allowNone) return setToast("Tick at least one repo with space, or esc to cancel");
1715
+ setModal(null);
1716
+ setFocus("list");
1717
+ apply((w) => m.fix.pickRepos(w, [...m.picked]));
1718
+ }
1719
+ return;
1720
+ }
1721
+ function fixallKeys(m, input, key) {
1722
+ if (key.upArrow) return setModal({ ...m, cursor: Math.max(0, m.cursor - 1) });
1723
+ if (key.downArrow) return setModal({ ...m, cursor: Math.min(m.items.length - 1, m.cursor + 1) });
1724
+ if (input === " ") return setModal({ ...m, items: m.items.map((it, i) => i === m.cursor ? { ...it, on: !it.on } : it) });
1725
+ if (key.return) {
1726
+ setModal(null);
1727
+ const chosen = m.items.filter((it) => it.on);
1728
+ apply((w) => runAll(w, chosen.map((it) => it.fix)));
1729
+ }
1730
+ return;
1731
+ }
1732
+ function addKeys(m, input, key) {
1733
+ const rows = addRows(world, m.target, m.query);
1734
+ const cursor2 = addCursor(rows, m.cursor);
1735
+ const step = (d) => {
1736
+ let i = cursor2 + d;
1737
+ while (rows[i]?.header) i += d;
1738
+ return rows[i] ? setModal({ ...m, cursor: i }) : void 0;
1739
+ };
1740
+ if (key.upArrow) return step(-1);
1741
+ if (key.downArrow) return step(1);
1742
+ if (key.backspace || key.delete) return setModal({ ...m, query: m.query.slice(0, -1), cursor: 0 });
1743
+ const members = rows[cursor2]?.members;
1744
+ if (input === " " && members) {
1745
+ const picked = new Set(m.picked);
1746
+ const all2 = members.every((n) => picked.has(n));
1747
+ for (const n of members) all2 ? picked.delete(n) : picked.add(n);
1748
+ return setModal({ ...m, picked });
1749
+ }
1750
+ if (key.return && m.target && m.picked.size) {
1751
+ const [target, names] = [m.target, [...m.picked]];
1752
+ setModal(null);
1753
+ return apply((w) => w.ops.addTo([target], names));
1754
+ }
1755
+ if (key.return && rows[cursor2]?.agent) {
1756
+ setModal(null);
1757
+ return ui.copy(writeSkillPrompt(world, m.query, m.target), "a prompt for an agent to write the skill");
1758
+ }
1759
+ if (key.return && rows[cursor2]?.run) {
1760
+ const row = rows[cursor2];
1761
+ if (row.create && !/^[a-z0-9]/.test(m.query)) return setToast("Type the new skill's name first");
1762
+ setModal(null);
1763
+ if (!row.create) return apply(row.run);
1764
+ const created = row.run(world);
1765
+ if (isFailure(created)) return setToast(`\u2717 ${said2(created)}`);
1766
+ return edit(world.ops.libraryFile(m.query), row.afterEdit);
1767
+ }
1768
+ if (isNameChar(input, key)) return setModal({ ...m, query: m.query + input.toLowerCase(), cursor: 0 });
1769
+ }
1770
+ const toastMax = Math.max(1, Math.min(Math.max(3, Math.floor(termRows * 0.4)), termRows - 8 - (sidebar ? 0 : 1)));
1747
1771
  const toastLines = toast && modal?.kind !== "confirm" ? wrap(toast, termCols - 2, toastMax).map((l) => " " + l) : [];
1748
1772
  const bottomH = Math.max(1, toastLines.length);
1749
1773
  const confirmLines = modal?.kind === "confirm" ? wrap(modal.fix.preview, columns - 6, 6) : [];
@@ -1979,7 +2003,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1979
2003
  list,
1980
2004
  split ? side : null
1981
2005
  ] });
1982
- 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`}`;
2006
+ 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 === "input" ? "enter confirm \xB7 ctrl+u clear \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`}`;
1983
2007
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: termCols, height: termRows, children: [
1984
2008
  sidebar ? null : topBar,
1985
2009
  /* @__PURE__ */ jsxs2(Box2, { children: [
@@ -2090,6 +2114,7 @@ function localSkill(root, s, git) {
2090
2114
  const state = git ? git.of(relativeTo(root, s.path)) : null;
2091
2115
  const others = otherCopies(root, s.name, s.location);
2092
2116
  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}`))));
2093
2118
  return {
2094
2119
  name: s.name,
2095
2120
  source: "lib",
@@ -2097,7 +2122,7 @@ function localSkill(root, s, git) {
2097
2122
  path: s.path,
2098
2123
  // Agents that actually load it here: visibility lists every agent you use, with `paths: 0` for the blind ones.
2099
2124
  agents: s.visibility.filter((v) => v.paths > 0).map((v) => v.id),
2100
- ...s.managed ? { version: s.version ?? void 0, ...managed[s.state] } : { ...other[s.state], ...own },
2125
+ ...s.managed ? { version: s.version ?? void 0, ...s.personal ? { personal: true } : {}, ...inGit ? { inGit: true } : {}, ...managed[s.state] } : { ...other[s.state], ...own },
2101
2126
  ...s.source ? { origin: `npx skills: ${s.source}` } : {},
2102
2127
  ...others.length ? { others } : {},
2103
2128
  git: state
@@ -2349,6 +2374,11 @@ function realOps(roots, backups, machine, libraryNames) {
2349
2374
  const c = updateProject(rootOf(repo), [name])[0];
2350
2375
  return c ? orForce(c, repo, "Update anyway", () => updateProject(rootOf(repo), [name], { force: true })[0]) : `${name} is up to date`;
2351
2376
  },
2377
+ pin: (repo, name, personal) => {
2378
+ 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`;
2381
+ },
2352
2382
  copyToLibrary: (repo, name) => ((s) => s ? toLibrary(s.path) : failed(`${name} isn't in ${repo} any more`))(find(repo, name)),
2353
2383
  deleteGlobal: (m) => {
2354
2384
  if (!entryExists(m.path)) return `${tildify(m.path)} is already gone`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skilllib",
3
- "version": "1.11.7",
3
+ "version": "1.12.0",
4
4
  "description": "Manage your agent skills like dependencies. One library for all your skills; each repo gets only the ones it needs, and every agent can see them. Works with Claude Code, Cursor, Codex, Zed and Grok.",
5
5
  "keywords": [
6
6
  "agent-skills",
@@ -6,7 +6,7 @@ allowed-tools: Bash(skilllib:*)
6
6
 
7
7
  # skilllib
8
8
 
9
- The user manages agent skills with the `skilllib` CLI. Their **library** (`~/.skilllib/library`, "my skills", "the registry") holds versioned skills that load nowhere until added to a repo. A repo pins its skills in `skilllib.json`.
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
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.
12
12
 
@@ -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>...` · `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 `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}`.
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.
@@ -33,7 +33,7 @@ Also: `usage --json` (Claude Code usage only, so "unused" is a hint; each skill
33
33
  - `inTheWay`: something else is where a link goes (a file, or another copy), so that agent may not run this copy there, and the command exits 1. Tell the user what's there; once they move it, `skilllib link` adds the link. (A skills folder that's a file or links out of the repo makes the skill `skipped` instead, with the reason.)
34
34
  - `blocked`: git tracks that folder. Re-run with `--allow-tracked` (for that command only) only with their OK.
35
35
  - `uncommitted`: git tracks that folder but not the skill's real copy, so a link there would break for teammates; `--allow-tracked` doesn't change that. Once the copy is committed, `link --allow-tracked` adds it.
36
- - Changes apply from the agent's next session. Remind the user to commit `skilllib.json` and the folders in `dirs`.
36
+ - Changes apply from the agent's next session. For a change without `personal`, remind the user to commit `skilllib.json` and the folders in `dirs`; a `personal` one has nothing to commit.
37
37
 
38
38
  **Prefer per-repo skills to global ones.** A global skill loads in every repo, for every agent. When `usage --json` shows one used in only a few repos, propose moving it there:
39
39