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.
@@ -48,6 +48,13 @@ function readJson(path) {
48
48
  return null;
49
49
  }
50
50
  }
51
+ function realNative(p) {
52
+ try {
53
+ return realpathSync.native(p);
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
51
58
  function realpathOrNull(path) {
52
59
  try {
53
60
  return realpathSync(path);
@@ -126,7 +133,7 @@ function localStamp(ms) {
126
133
  }
127
134
 
128
135
  // src/config.ts
129
- import { existsSync as existsSync5, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
136
+ import { existsSync as existsSync5, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
130
137
  import { join as join6, resolve as resolve4 } from "path";
131
138
 
132
139
  // src/harnesses.ts
@@ -225,8 +232,10 @@ function installDirs(enabled, keep) {
225
232
  }
226
233
 
227
234
  // src/project.ts
228
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
229
- import { dirname as dirname2, join as join5, resolve as resolve3 } from "path";
235
+ import { execFileSync } from "child_process";
236
+ import { createHash as createHash2 } from "crypto";
237
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, renameSync as renameSync2, rmSync as rmSync3, statSync as statSync3, writeFileSync as writeFileSync2 } from "fs";
238
+ import { basename as basename2, dirname as dirname2, join as join5, resolve as resolve3 } from "path";
230
239
 
231
240
  // src/versions.ts
232
241
  import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
@@ -356,8 +365,7 @@ function projectHere(from = process.cwd()) {
356
365
  const root = findProjectRoot(from);
357
366
  return isProjectCandidate(root) && (existsSync4(join5(root, ".git")) || existsSync4(join5(root, MANIFEST_FILE))) ? root : null;
358
367
  }
359
- function readManifest(root) {
360
- const path = join5(root, MANIFEST_FILE);
368
+ function readPins(path) {
361
369
  if (!existsSync4(path)) return { skills: {} };
362
370
  let data;
363
371
  try {
@@ -393,9 +401,141 @@ function readManifest(root) {
393
401
  );
394
402
  return { ...data, skills };
395
403
  }
404
+ function readManifest(root) {
405
+ const shared = readPins(join5(root, MANIFEST_FILE));
406
+ const file = personalPinsFile(root);
407
+ const personal = file ? readPins(file).skills : {};
408
+ const mine = Object.fromEntries(Object.entries(personal).map(([name, dep]) => [name, { ...dep, personal: true }]));
409
+ return { ...shared, skills: { ...mine, ...shared.skills } };
410
+ }
411
+ var sorted = (skills) => Object.fromEntries(Object.entries(skills).sort(([a], [b]) => a.localeCompare(b, "en")));
396
412
  function writeManifest(root, manifest) {
397
- const skills = Object.fromEntries(Object.entries(manifest.skills).sort(([a], [b]) => a.localeCompare(b, "en")));
398
- writeState(join5(root, MANIFEST_FILE), { ...manifest, skills });
413
+ const shared = Object.fromEntries(Object.entries(manifest.skills).filter(([, d]) => !d.personal));
414
+ const mine = Object.fromEntries(Object.entries(manifest.skills).flatMap(([name, d]) => d.personal ? [[name, withoutMark(d)]] : []));
415
+ const path = join5(root, MANIFEST_FILE);
416
+ const onDisk = readPins(path).skills;
417
+ const leftEmpty = !Object.keys(shared).length && Object.keys(mine).length > 0 && existsSync4(path) && gitIn(root, ["ls-files", "--", MANIFEST_FILE]) === "";
418
+ if (leftEmpty) rmSync3(path, { force: true });
419
+ else if (existsSync4(path) || Object.keys(shared).length) writeChanged(path, { ...manifest, skills: sorted(shared) });
420
+ const file = personalPinsFile(root);
421
+ if (!file) {
422
+ if (Object.keys(mine).length) throw new Error(`${root} isn't the top folder of a git repo, so it can't have personal pins`);
423
+ return;
424
+ }
425
+ for (const [name, dep] of Object.entries(readPins(file).skills)) if (onDisk[name] && manifest.skills[name] && !manifest.skills[name].personal) mine[name] = dep;
426
+ if (Object.keys(mine).length) writeChanged(file, { ...pinsHeader(repoOf(root)), skills: sorted(mine) });
427
+ else rmSync3(file, { force: true });
428
+ excludePersonal(root, mine);
429
+ }
430
+ function writeChanged(path, data) {
431
+ try {
432
+ if (JSON.stringify(JSON.parse(readFileSync3(path, "utf-8"))) === JSON.stringify(data)) return;
433
+ } catch {
434
+ }
435
+ writeState(path, data);
436
+ }
437
+ function withoutMark(dep) {
438
+ const { personal: _, ...rest } = dep;
439
+ return rest;
440
+ }
441
+ function hasPins(root) {
442
+ if (existsSync4(join5(root, MANIFEST_FILE))) return true;
443
+ const file = personalPinsFile(root);
444
+ return !!file && existsSync4(file);
445
+ }
446
+ function personalPinsDir() {
447
+ return join5(skilllibHome(), "pins");
448
+ }
449
+ function gitIn(root, args) {
450
+ try {
451
+ return execFileSync("git", args, { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
452
+ } catch {
453
+ return null;
454
+ }
455
+ }
456
+ function normalizeRemote(url) {
457
+ let s = url.trim();
458
+ const scp = s.match(/^(?:[^/@:]+@)?([^/:\\]{2,}):(?!\/)(.*)$/);
459
+ if (scp) s = `${scp[1]}/${scp[2]}`;
460
+ else s = s.replace(/^[a-z+]+:\/\//i, "").replace(/^[^/@]+@/, "").replace(/^([^/:]+):\d+\//, "$1/");
461
+ s = s.replace(/\.git\/?$/, "").replace(/\/+$/, "");
462
+ return s.replace(/^[^/]*/, (host) => host.toLowerCase());
463
+ }
464
+ var repos = /* @__PURE__ */ new Map();
465
+ var changedAt = (path) => statSync3(path, { throwIfNoEntry: false })?.mtimeMs ?? -1;
466
+ function repoOf(root) {
467
+ const real = realPath(root);
468
+ const hit = repos.get(real);
469
+ if (hit && (hit.config ? changedAt(hit.config) === hit.seen : !existsSync4(join5(root, ".git")))) return hit.repo;
470
+ const out2 = gitIn(root, ["rev-parse", "--show-prefix", "--git-common-dir"])?.split("\n");
471
+ if (!out2) {
472
+ repos.set(real, { repo: null, config: null, seen: 0 });
473
+ return null;
474
+ }
475
+ const common = realNative(resolve3(root, out2[1].trim())) ?? resolve3(root, out2[1].trim());
476
+ const folder = basename2(common) === ".git" ? dirname2(common) : common;
477
+ const config = join5(common, "config");
478
+ const seen = changedAt(config);
479
+ let repo = null;
480
+ if (out2[0] === "") {
481
+ const urls = (gitIn(root, ["config", "--get-regexp", "^remote\\..*\\.url$"]) ?? "").split("\n").filter(Boolean).map((l) => l.split(" "));
482
+ const url = (urls.find(([k]) => k === "remote.origin.url") ?? urls[0])?.[1];
483
+ const remote = url ? normalizeRemote(url) : void 0;
484
+ repo = { file: pinsFileFor(root, remote, folder), folder, common, ...remote ? { remote } : {}, root };
485
+ }
486
+ repos.set(real, { repo, config, seen });
487
+ return repo;
488
+ }
489
+ var firstCommit = (root) => gitIn(root, ["rev-list", "--max-parents=0", "HEAD"])?.split("\n")[0] || null;
490
+ function pinsFileFor(root, remote, folder) {
491
+ const name = remote ?? `local-${basename2(folder)}`;
492
+ const slug = name.replace(/[^a-z0-9._-]+/gi, "-").replace(/^-+|-+$/g, "").slice(0, 80);
493
+ const file = join5(personalPinsDir(), `${slug}-${createHash2("sha1").update(remote ?? folder).digest("hex").slice(0, 8)}.json`);
494
+ if (existsSync4(file) || !existsSync4(personalPinsDir())) return file;
495
+ for (const other of readdirSync2(personalPinsDir()).filter((f) => f.endsWith(".json"))) {
496
+ const at = join5(personalPinsDir(), other);
497
+ const theirs = readJson(at);
498
+ if (theirs?.path === folder && (theirs.first ? theirs.first === firstCommit(root) : !theirs.remote)) {
499
+ renameSync2(at, file);
500
+ break;
501
+ }
502
+ }
503
+ return file;
504
+ }
505
+ var pinsHeader = (repo) => ((first) => ({ ...repo.remote ? { remote: repo.remote } : {}, path: repo.folder, ...first ? { first } : {} }))(firstCommit(repo.root));
506
+ function personalPinsFile(root) {
507
+ return repoOf(root)?.file ?? null;
508
+ }
509
+ function personalPins(root) {
510
+ const file = personalPinsFile(root);
511
+ return file ? readPins(file).skills : {};
512
+ }
513
+ function dropPersonalPin(root, name) {
514
+ const file = personalPinsFile(root);
515
+ if (!file) return;
516
+ const { [name]: _, ...rest } = readPins(file).skills;
517
+ if (Object.keys(rest).length) writeChanged(file, { ...pinsHeader(repoOf(root)), skills: sorted(rest) });
518
+ else rmSync3(file, { force: true });
519
+ excludePersonal(root);
520
+ }
521
+ var EXCLUDE_START = "# skilllib: personal skills (kept by skilllib; don't edit)";
522
+ var EXCLUDE_END = "# end skilllib";
523
+ function excludePersonal(root, skills = Object.fromEntries(Object.entries(readManifest(root).skills).filter(([, d]) => d.personal))) {
524
+ const repo = repoOf(root);
525
+ if (!repo?.file) return;
526
+ const file = join5(repo.common, "info", "exclude");
527
+ const text = existsSync4(file) ? readFileSync3(file, "utf-8") : "";
528
+ const lines = text.split("\n");
529
+ const start = lines.indexOf(EXCLUDE_START);
530
+ const end = start === -1 ? -1 : lines.indexOf(EXCLUDE_END, start);
531
+ const rest = start === -1 ? lines : [...lines.slice(0, start), ...lines.slice(end === -1 ? start + 1 : end + 1)];
532
+ while (rest.length && rest[rest.length - 1] === "") rest.pop();
533
+ const paths = Object.entries(skills).flatMap(([name, d]) => [d.dir ?? PROJECT_SKILLS_DIR, ...d.links ?? []].map((dir) => `/${dir}/${name.replace(/[[\]*?\\]/g, "\\$&")}`));
534
+ const block = paths.length ? [EXCLUDE_START, ...[...new Set(paths)].sort(), EXCLUDE_END] : [];
535
+ const next = [...rest, ...rest.length && block.length ? [""] : [], ...block].join("\n") + (rest.length || block.length ? "\n" : "");
536
+ if (next === text) return;
537
+ mkdirSync3(dirname2(file), { recursive: true });
538
+ writeFileSync2(file, next);
399
539
  }
400
540
  function projectsFile() {
401
541
  return join5(skilllibHome(), "projects.json");
@@ -454,8 +594,9 @@ function configFile() {
454
594
  return join6(skilllibHome(), "config.json");
455
595
  }
456
596
  function readConfig() {
457
- const data = readState(configFile()) ?? {};
458
- return { ...data, roots: data.roots ?? [], hidden: data.hidden ?? [] };
597
+ const { agentsDirOk, ...data } = readState(configFile()) ?? {};
598
+ const allowTracked = [.../* @__PURE__ */ new Set([...data.allowTracked ?? [], ...agentsDirOk ?? []])].sort();
599
+ return { ...data, roots: data.roots ?? [], hidden: data.hidden ?? [], ...allowTracked.length ? { allowTracked } : {} };
459
600
  }
460
601
  function writeConfig(config) {
461
602
  writeState(configFile(), config);
@@ -466,7 +607,7 @@ function expandHome(path) {
466
607
  function addRoot(path) {
467
608
  const root = expandHome(path);
468
609
  if (!existsSync5(root)) throw new Error(`${tildify(root)} doesn't exist`);
469
- if (!statSync3(root).isDirectory()) throw new Error(`${tildify(root)} isn't a folder`);
610
+ if (!statSync4(root).isDirectory()) throw new Error(`${tildify(root)} isn't a folder`);
470
611
  const config = readConfig();
471
612
  if (!config.roots.includes(root)) writeConfig({ ...config, roots: [...config.roots, root].sort() });
472
613
  return root;
@@ -495,7 +636,7 @@ function findProjects(dir, depth = 4) {
495
636
  }
496
637
  let entries = [];
497
638
  try {
498
- entries = readdirSync2(dir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".") && !SKIP_DIRS.has(d.name)).map((d) => join6(dir, d.name));
639
+ entries = readdirSync3(dir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".") && !SKIP_DIRS.has(d.name)).map((d) => join6(dir, d.name));
499
640
  } catch {
500
641
  return [];
501
642
  }
@@ -521,7 +662,10 @@ function setHarnesses(ids) {
521
662
  }
522
663
  function allowTrackedLinks(root) {
523
664
  const config = readConfig();
524
- writeConfig({ ...config, agentsDirOk: [.../* @__PURE__ */ new Set([...config.agentsDirOk ?? [], root])].sort() });
665
+ writeConfig({ ...config, allowTracked: [.../* @__PURE__ */ new Set([...config.allowTracked ?? [], root])].sort() });
666
+ }
667
+ function allowsTrackedLinks(root) {
668
+ return readConfig().allowTracked?.includes(root) ?? false;
525
669
  }
526
670
  function keptGlobal() {
527
671
  return new Set(readConfig().keepGlobal ?? []);
@@ -538,17 +682,17 @@ function setKeepGlobal(names, keep) {
538
682
  }
539
683
 
540
684
  // src/agentSkill.ts
541
- import { existsSync as existsSync8, lstatSync as lstatSync3, mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync as rmSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
685
+ import { existsSync as existsSync8, lstatSync as lstatSync3, mkdirSync as mkdirSync6, readFileSync as readFileSync5, rmSync as rmSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
542
686
  import { join as join9 } from "path";
543
687
 
544
688
  // src/library.ts
545
- import { execFileSync } from "child_process";
546
- import { cpSync as cpSync2, existsSync as existsSync7, lstatSync as lstatSync2, mkdirSync as mkdirSync4, readdirSync as readdirSync4, readFileSync as readFileSync4, readlinkSync as readlinkSync2, renameSync as renameSync2, rmSync as rmSync3, symlinkSync, unlinkSync, writeFileSync as writeFileSync2, realpathSync as realpathSync3 } from "fs";
547
- import { basename as basename3, dirname as dirname3, isAbsolute, join as join8, posix, relative as relative2, resolve as resolve6, sep as sep4 } from "path";
689
+ import { execFileSync as execFileSync2 } from "child_process";
690
+ import { cpSync as cpSync2, existsSync as existsSync7, lstatSync as lstatSync2, mkdirSync as mkdirSync5, readdirSync as readdirSync5, readFileSync as readFileSync4, readlinkSync as readlinkSync2, renameSync as renameSync3, rmSync as rmSync4, symlinkSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
691
+ import { basename as basename4, dirname as dirname3, isAbsolute, join as join8, posix, relative as relative2, resolve as resolve6, sep as sep4 } from "path";
548
692
 
549
693
  // src/sources.ts
550
- import { existsSync as existsSync6, lstatSync, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readlinkSync } from "fs";
551
- import { basename as basename2, join as join7, resolve as resolve5, sep as sep3 } from "path";
694
+ import { existsSync as existsSync6, lstatSync, mkdirSync as mkdirSync4, readdirSync as readdirSync4, readlinkSync } from "fs";
695
+ import { basename as basename3, join as join7, resolve as resolve5, sep as sep3 } from "path";
552
696
  import { tmpdir } from "os";
553
697
  function skillsShLock() {
554
698
  return readJson(join7(userHome(), ".agents", ".skill-lock.json"))?.skills ?? {};
@@ -557,7 +701,7 @@ function projectSkillsLock(root) {
557
701
  return readJson(join7(root, "skills-lock.json"))?.skills ?? {};
558
702
  }
559
703
  function vendorSkill(path, kind, origin, harnesses) {
560
- return { name: basename2(path), kind, origin, path, description: readSkillInfo(path).description, movable: false, broken: false, harnesses, links: [] };
704
+ return { name: basename3(path), kind, origin, path, description: readSkillInfo(path).description, movable: false, broken: false, harnesses, links: [] };
561
705
  }
562
706
  function harnessesReading(dir, enabled) {
563
707
  const real = realPath(dir);
@@ -588,7 +732,7 @@ function globalFolderSkills(enabled) {
588
732
  const agentsDir = join7(userHome(), ".agents", "skills");
589
733
  const entries = globalSkillDirs().flatMap((dir) => {
590
734
  if (!isDir(dir)) return [];
591
- return readdirSync3(dir).filter((name) => !name.startsWith(".") && name !== "synced").flatMap((name) => {
735
+ return readdirSync4(dir).filter((name) => !name.startsWith(".") && name !== "synced").flatMap((name) => {
592
736
  const path = join7(dir, name);
593
737
  const isLink2 = lstatSync(path).isSymbolicLink();
594
738
  const broken = !existsSync6(join7(path, "SKILL.md"));
@@ -656,7 +800,7 @@ function claudeAiSkills(enabled) {
656
800
  byReal.set(real, entry);
657
801
  }
658
802
  return [...byReal.values()].flatMap(
659
- ({ dir, readers }) => (isDir(dir) ? readdirSync3(dir) : []).filter((d) => !d.startsWith(".")).flatMap(
803
+ ({ dir, readers }) => (isDir(dir) ? readdirSync4(dir) : []).filter((d) => !d.startsWith(".")).flatMap(
660
804
  (bucket) => skillDirsIn(join7(dir, bucket)).map((path) => vendorSkill(path, "claude.ai", "claude.ai account", enabled.filter((id) => readers.includes(id))))
661
805
  )
662
806
  );
@@ -700,8 +844,8 @@ function pluginSkills(harnesses, root) {
700
844
  return loadedBy.length ? skillDirsIn(join7(dir, "skills")).map((path) => ({ path, origin: id, loadedBy })) : [];
701
845
  });
702
846
  const syncedDir = join7(plugins, "synced");
703
- const fromSynced = claude2 && existsSync6(syncedDir) ? readdirSync3(syncedDir).filter((b) => !b.startsWith(".") && isDir(join7(syncedDir, b))).flatMap(
704
- (bucket) => readdirSync3(join7(syncedDir, bucket), { withFileTypes: true }).filter((d) => d.isDirectory()).flatMap((d) => {
847
+ const fromSynced = claude2 && existsSync6(syncedDir) ? readdirSync4(syncedDir).filter((b) => !b.startsWith(".") && isDir(join7(syncedDir, b))).flatMap(
848
+ (bucket) => readdirSync4(join7(syncedDir, bucket), { withFileTypes: true }).filter((d) => d.isDirectory()).flatMap((d) => {
705
849
  const root2 = join7(syncedDir, bucket, d.name);
706
850
  const name = readJson(join7(root2, ".claude-plugin", "plugin.json"))?.name ?? d.name;
707
851
  if (enabled[`${name}@synced`] === false) return [];
@@ -736,7 +880,7 @@ function libraryOrigins() {
736
880
  function recordOrigin(name, origin) {
737
881
  const origins = libraryOrigins();
738
882
  if (origins[name] === origin) return;
739
- mkdirSync3(skilllibHome(), { recursive: true });
883
+ mkdirSync4(skilllibHome(), { recursive: true });
740
884
  writeState(originsFile(), { ...origins, [name]: origin });
741
885
  }
742
886
  function originFor(dir) {
@@ -749,14 +893,14 @@ function originFor(dir) {
749
893
  if (match) return match.kind === "global" ? "global folder" : `${match.kind}: ${match.origin}`;
750
894
  const project = dir.split(/[\\/]\.(?:claude|agents|grok|cursor|codex)[\\/]skills[\\/]/)[0];
751
895
  if (!project || project === dir) return dir;
752
- const locked = projectSkillsLock(project)[basename2(dir)]?.source;
753
- return locked ? `skills.sh: ${locked}` : `project: ${basename2(project)}`;
896
+ const locked = projectSkillsLock(project)[basename3(dir)]?.source;
897
+ return locked ? `skills.sh: ${locked}` : `project: ${basename3(project)}`;
754
898
  }
755
899
 
756
900
  // src/library.ts
757
901
  function librarySkills() {
758
902
  return skillDirsIn(libraryDir()).map((dir) => ({
759
- name: basename3(dir),
903
+ name: basename4(dir),
760
904
  description: readSkillInfo(dir).description,
761
905
  dir
762
906
  }));
@@ -840,13 +984,6 @@ function visibilityOf(root, name, enabled = enabledHarnesses()) {
840
984
  paths: harness(id).projectDirs.filter((dir) => existsSync7(join8(root, dir, name, "SKILL.md"))).length
841
985
  }));
842
986
  }
843
- function realNative(p) {
844
- try {
845
- return realpathSync3.native(p);
846
- } catch {
847
- return null;
848
- }
849
- }
850
987
  function survives(path, gone) {
851
988
  return lostAt(path, gone) === null;
852
989
  }
@@ -854,10 +991,10 @@ function lostAt(path, gone) {
854
991
  const key = (p) => process.platform === "win32" ? p.toLowerCase() : p;
855
992
  const canon = (p) => {
856
993
  const dir = realNative(dirname3(p)) ?? dirname3(p);
857
- const leaf = basename3(p);
994
+ const leaf = basename4(p);
858
995
  try {
859
996
  lstatSync2(join8(dir, leaf));
860
- const names = readdirSync4(dir);
997
+ const names = readdirSync5(dir);
861
998
  return join8(dir, names.includes(leaf) ? leaf : names.find((n) => n.toLowerCase() === leaf.toLowerCase()) ?? leaf);
862
999
  } catch {
863
1000
  return join8(dir, leaf);
@@ -925,11 +1062,11 @@ function blindWithoutGlobal(name, copies, roots, nestedOf = (root) => nestedEntr
925
1062
  };
926
1063
  return [...byWho.values()].filter((b) => !servedAbove(b));
927
1064
  }
928
- function strandedLinks(name, copies, roots, label = basename3) {
1065
+ function strandedLinks(name, copies, roots, label = basename4) {
929
1066
  const gone = copies.flatMap((c) => [c.path, ...c.links ?? []]);
930
1067
  const unique = [...new Map(roots.map((r) => [realpathOrNull(r) ?? r, r])).values()];
931
1068
  const seen = /* @__PURE__ */ new Set();
932
- const once = (p) => ((k) => seen.has(k) ? false : (seen.add(k), true))(join8(realNative(dirname3(p)) ?? dirname3(p), basename3(p)));
1069
+ const once = (p) => ((k) => seen.has(k) ? false : (seen.add(k), true))(join8(realNative(dirname3(p)) ?? dirname3(p), basename4(p)));
933
1070
  return unique.flatMap((root) => [
934
1071
  ...ALL_PROJECT_DIRS.filter(
935
1072
  (d) => writableDir(root, d) && isLink(join8(root, d, name)) && existsSync7(join8(root, d, name, "SKILL.md")) && !survives(join8(root, d, name), gone) && once(join8(root, d, name))
@@ -942,7 +1079,7 @@ function strandedLinks(name, copies, roots, label = basename3) {
942
1079
  function copiesDiffer(paths) {
943
1080
  return new Set(paths.map((p) => treeHash(p)).filter((h) => h !== null)).size > 1;
944
1081
  }
945
- function blindSaid(name, blind, label = basename3) {
1082
+ function blindSaid(name, blind, label = basename4) {
946
1083
  const who = blind.map((b) => `${harness(b.id).name} in ${label(b.root)}`).join(", ");
947
1084
  const folders2 = [...new Set(blind.flatMap((b) => b.folder ? [`${b.folder} in ${label(b.root)}`] : []))];
948
1085
  const links = [...new Set(blind.flatMap((b) => b.link ? [b.link.startsWith("~") || isAbsolute(b.link) ? b.link : `${b.link} in ${label(b.root)}`] : []))];
@@ -955,14 +1092,14 @@ function blindSaid(name, blind, label = basename3) {
955
1092
  const shared = [...sharedBy.values()].map(
956
1093
  (g) => `put a copy of ${name} in ${g.to} (${[...g.where].join(", ")} link${g.where.size === 1 ? "s" : ""} there), or point ${g.where.size === 1 ? `${[...g.dirs][0]} back into the repo` : "those folders back into their repos"}`
957
1094
  );
958
- const repos = blind.filter((b) => !b.folder && !b.link && !b.shared);
1095
+ const repos2 = blind.filter((b) => !b.folder && !b.link && !b.shared);
959
1096
  const fixes = [
960
1097
  ...folders2.length ? [`point ${folders2.join(", ")} back into the repo`] : [],
961
1098
  // A package's link to the global copy: add or link at the repo's top doesn't touch it.
962
1099
  ...links.length ? [`give ${links.join(", ")} ${links.length === 1 ? "a copy of its own" : "each a copy of its own"} instead of a link to the global one`] : [],
963
1100
  ...shared,
964
1101
  // Named, so with other advice beside it, it's clear which repo (or package) this is about.
965
- ...repos.length ? ((names) => [`give ${names.join(", ")} ${names.length === 1 ? "a copy of its own" : "their own copies"} (skilllib add, sync or link there)`])([...new Set(repos.map((b) => label(b.root)))]) : []
1102
+ ...repos2.length ? ((names) => [`give ${names.join(", ")} ${names.length === 1 ? "a copy of its own" : "their own copies"} (skilllib add, sync or link there)`])([...new Set(repos2.map((b) => label(b.root)))]) : []
966
1103
  ];
967
1104
  return `${who} only ${blind.length === 1 ? "sees" : "see"} the global copy: ${fixes.join("; ")}, or keep ${name} global (skilllib global keep)`;
968
1105
  }
@@ -997,7 +1134,7 @@ function pinsSkill(root, name) {
997
1134
  }
998
1135
  function isGitTracked(root, dir) {
999
1136
  try {
1000
- const out2 = execFileSync("git", ["ls-files", "--", dir], { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
1137
+ const out2 = execFileSync2("git", ["ls-files", "--", dir], { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
1001
1138
  return out2.trim().length > 0;
1002
1139
  } catch {
1003
1140
  return false;
@@ -1024,7 +1161,8 @@ function projectStatus(root) {
1024
1161
  latest: latest?.version ?? null,
1025
1162
  location,
1026
1163
  path,
1027
- visibility: visibilityOf(root, name, enabled)
1164
+ visibility: visibilityOf(root, name, enabled),
1165
+ ...dep.personal ? { personal: true } : {}
1028
1166
  };
1029
1167
  });
1030
1168
  const seen = new Set(Object.keys(skills));
@@ -1033,7 +1171,7 @@ function projectStatus(root) {
1033
1171
  const byName = /* @__PURE__ */ new Map();
1034
1172
  for (const dir of ALL_PROJECT_DIRS.filter((d) => writableDir(root, d))) {
1035
1173
  for (const path of skillDirsIn(join8(root, dir))) {
1036
- const name = basename3(path);
1174
+ const name = basename4(path);
1037
1175
  if (seen.has(name)) continue;
1038
1176
  byName.set(name, [...byName.get(name) ?? [], path]);
1039
1177
  }
@@ -1089,7 +1227,7 @@ function nestedEntries(root, { depth = PACKAGE_DEPTH, enabled = enabledHarnesses
1089
1227
  const found = packageSkillFolders(root, subs ?? packageDirs(root, depth)).flatMap(
1090
1228
  ({ sub, skillsDir, location }) => skillDirsIn(join8(sub, skillsDir)).flatMap((path) => {
1091
1229
  if (isLink(path) && (realpathOrNull(path) ?? "").startsWith(top + sep4)) return [];
1092
- const name = basename3(path);
1230
+ const name = basename4(path);
1093
1231
  const readers = NESTED_READERS.filter(([d]) => realpathOrNull(join8(sub, d, name)) === realpathOrNull(path)).flatMap(([, r]) => r);
1094
1232
  return [{ name, path, location, skillsDir, readers: enabled.filter((id) => readers.includes(id)) }];
1095
1233
  })
@@ -1102,7 +1240,7 @@ function packageDirs(root, depth = PACKAGE_DEPTH) {
1102
1240
  const walk = (dir, left) => {
1103
1241
  let subs = [];
1104
1242
  try {
1105
- subs = readdirSync4(dir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".") && !BUILD_DIRS.has(d.name)).map((d) => join8(dir, d.name));
1243
+ subs = readdirSync5(dir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".") && !BUILD_DIRS.has(d.name)).map((d) => join8(dir, d.name));
1106
1244
  } catch {
1107
1245
  return;
1108
1246
  }
@@ -1128,11 +1266,14 @@ function sharedByGit(root, location, name) {
1128
1266
  const copy = `${location}/${name}`;
1129
1267
  if (isGitTracked(root, copy)) return true;
1130
1268
  if (!isGitTracked(root, location)) return false;
1269
+ return !gitIgnores(root, copy);
1270
+ }
1271
+ function gitIgnores(root, rel) {
1131
1272
  try {
1132
- execFileSync("git", ["check-ignore", "-q", "--no-index", "--", copy], { cwd: root, stdio: "ignore" });
1133
- return false;
1134
- } catch {
1273
+ execFileSync2("git", ["check-ignore", "-q", "--no-index", "--", rel], { cwd: root, stdio: "ignore" });
1135
1274
  return true;
1275
+ } catch {
1276
+ return false;
1136
1277
  }
1137
1278
  }
1138
1279
  function linkEverywhere(root, name, location, { allowTracked = false, dryRun = false } = {}) {
@@ -1155,19 +1296,19 @@ function linkEverywhere(root, name, location, { allowTracked = false, dryRun = f
1155
1296
  stuck(`${dir}/${name}`);
1156
1297
  continue;
1157
1298
  }
1158
- if (isGitTracked(root, dir)) {
1299
+ if (isGitTracked(root, dir) && !gitIgnores(root, `${dir}/${name}`)) {
1159
1300
  if (!(shared ??= sharedByGit(root, location, name))) {
1160
1301
  if (!result.uncommitted.includes(dir)) result.uncommitted.push(dir);
1161
1302
  continue;
1162
1303
  }
1163
- if (!allowTracked && !readConfig().agentsDirOk?.includes(root)) {
1304
+ if (!allowTracked && !allowsTrackedLinks(root)) {
1164
1305
  if (!result.blocked.includes(dir)) result.blocked.push(dir);
1165
1306
  continue;
1166
1307
  }
1167
1308
  }
1168
1309
  if (!dryRun) {
1169
1310
  if (broken) unlinkSync(link2);
1170
- mkdirSync4(join8(root, dir), { recursive: true });
1311
+ mkdirSync5(join8(root, dir), { recursive: true });
1171
1312
  linkDir(join8(root, location, name), link2);
1172
1313
  }
1173
1314
  result.created.push(dir);
@@ -1186,11 +1327,12 @@ function relinkDependency(root, name, { allowTracked = false } = {}) {
1186
1327
  const manifest = readManifest(root);
1187
1328
  const dep = manifest.skills[name];
1188
1329
  if (!dep) return { created: [], blocked: [], uncommitted: [], inTheWay: [] };
1330
+ if (dep.personal) excludePersonal(root, { ...personalOf(manifest), [name]: { ...dep, links: [...ALL_PROJECT_DIRS] } });
1189
1331
  const result = linkEverywhere(root, name, dep.dir ?? PROJECT_SKILLS_DIR, { allowTracked });
1190
1332
  if (result.created.length) {
1191
1333
  manifest.skills[name] = { ...dep, links: [.../* @__PURE__ */ new Set([...dep.links ?? [], ...result.created])] };
1192
1334
  writeManifest(root, manifest);
1193
- }
1335
+ } else if (dep.personal) excludePersonal(root);
1194
1336
  return result;
1195
1337
  }
1196
1338
  function linkAll(root, { allowTracked = false, names } = {}) {
@@ -1237,17 +1379,30 @@ function addRefusal(root, name, hash) {
1237
1379
  const plan = addPlan(root, name, hash, false);
1238
1380
  return "reason" in plan ? plan.reason : null;
1239
1381
  }
1240
- function addSkill(root, name, { force = false, version, allowTracked = false } = {}) {
1382
+ function addSkill(root, name, { force = false, version, allowTracked = false, personal } = {}) {
1241
1383
  if (!name || name === "." || name === ".." || /[\\/]/.test(name)) return { name, action: "skipped", reason: "not a skill name" };
1242
- const target = version === void 0 ? latestVersion(name) : getVersion(name, version);
1384
+ const before = personal !== void 0 && version === void 0 ? readManifest(root).skills[name] : void 0;
1385
+ const moving = !!before && !!before.personal !== personal;
1386
+ const keep = moving ? versionForHash(name, before.hash) : null;
1387
+ if (moving && !keep) return { name, action: "skipped", reason: `your library has no copy of the version it pins, so its pin can't move as it is: skilllib update ${name} first` };
1388
+ const target = keep ?? (version === void 0 ? latestVersion(name) : getVersion(name, version));
1243
1389
  if (!target) return { name, action: "skipped", reason: version === void 0 ? "not in the library" : `version ${version} doesn't exist` };
1244
1390
  const plan = addPlan(root, name, target.hash, force);
1245
1391
  if ("reason" in plan) return { name, action: "skipped", reason: plan.reason };
1246
1392
  const { manifest, recorded, primary, wantedLinks, to, local } = plan;
1393
+ const mine = pinsWhere(root, recorded, personal);
1394
+ if (typeof mine === "string") return { name, action: "skipped", reason: mine };
1395
+ const committed = [primary, ...recorded?.links ?? []].find((d) => isGitTracked(root, `${d}/${name}`));
1396
+ if (mine && !recorded?.personal && committed)
1397
+ return { name, action: "skipped", reason: `${committed}/${name} is committed to git, so it can't be personal: remove it from git first (git rm -r --cached '${`${committed}/${name}`.replace(/'/g, "'\\''")}')` };
1398
+ if (mine || recorded?.personal) {
1399
+ const { [name]: _, ...others } = personalOf(manifest);
1400
+ excludePersonal(root, mine ? { ...others, [name]: { version: target.version, hash: target.hash, dir: primary, links: wantedLinks } } : others);
1401
+ }
1247
1402
  let backedUp;
1248
1403
  const copied = local !== target.hash || isLink(to);
1249
1404
  if (copied) {
1250
- mkdirSync4(join8(root, primary), { recursive: true });
1405
+ mkdirSync5(join8(root, primary), { recursive: true });
1251
1406
  if (isLink(to)) unlinkSync(to);
1252
1407
  else if (local !== null && versionForHash(name, local) === null) backedUp = stash(to, "edit-backup");
1253
1408
  copySkill(versionDir(name, target.version), to);
@@ -1258,11 +1413,19 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
1258
1413
  const inTheWay = [];
1259
1414
  let shared;
1260
1415
  let relinked = false;
1416
+ const toShared = !!recorded?.personal && !mine;
1417
+ const heldBack = (dir) => {
1418
+ if (!isGitTracked(root, dir) || gitIgnores(root, `${dir}/${name}`)) return false;
1419
+ if (!(shared ??= sharedByGit(root, primary, name))) return uncommitted.push(dir), true;
1420
+ if (!allowTracked && !allowsTrackedLinks(root)) return blocked.push(dir), true;
1421
+ return false;
1422
+ };
1261
1423
  for (const dir of wantedLinks) {
1262
1424
  const link2 = join8(root, dir, name);
1263
1425
  const slot = slotAt(link2, to);
1264
1426
  if (slot === "ok") {
1265
- links.push(dir);
1427
+ if (toShared && isLink(link2) && heldBack(dir)) unlinkSync(link2);
1428
+ else links.push(dir);
1266
1429
  continue;
1267
1430
  }
1268
1431
  if (slot === "taken") {
@@ -1270,18 +1433,9 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
1270
1433
  if (recorded?.links?.includes(dir)) links.push(dir);
1271
1434
  continue;
1272
1435
  }
1273
- if (!recorded?.links?.includes(dir) && isGitTracked(root, dir)) {
1274
- if (!(shared ??= sharedByGit(root, primary, name))) {
1275
- uncommitted.push(dir);
1276
- continue;
1277
- }
1278
- if (!allowTracked && !readConfig().agentsDirOk?.includes(root)) {
1279
- blocked.push(dir);
1280
- continue;
1281
- }
1282
- }
1436
+ if ((toShared || !recorded?.links?.includes(dir)) && heldBack(dir)) continue;
1283
1437
  if (isLink(link2)) unlinkSync(link2);
1284
- mkdirSync4(join8(root, dir), { recursive: true });
1438
+ mkdirSync5(join8(root, dir), { recursive: true });
1285
1439
  linkDir(to, link2);
1286
1440
  links.push(dir);
1287
1441
  relinked = true;
@@ -1290,9 +1444,11 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
1290
1444
  version: target.version,
1291
1445
  hash: target.hash,
1292
1446
  ...primary !== PROJECT_SKILLS_DIR ? { dir: primary } : {},
1293
- ...links.length ? { links } : {}
1447
+ ...links.length ? { links } : {},
1448
+ ...mine ? { personal: true } : {}
1294
1449
  };
1295
1450
  writeManifest(root, manifest);
1451
+ const moved = recorded && !!recorded.personal !== mine ? mine ? "personal" : "shared" : void 0;
1296
1452
  const from = recorded ? versionForHash(name, recorded.hash)?.version ?? recorded.version : void 0;
1297
1453
  return {
1298
1454
  name,
@@ -1308,9 +1464,16 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
1308
1464
  ...blocked.length ? { blocked } : {},
1309
1465
  ...uncommitted.length ? { uncommitted } : {},
1310
1466
  ...inTheWay.length ? { inTheWay } : {},
1311
- ...backedUp ? { backedUp } : {}
1467
+ ...backedUp ? { backedUp } : {},
1468
+ ...moved ? { moved } : {}
1312
1469
  };
1313
1470
  }
1471
+ function pinsWhere(root, recorded, personal) {
1472
+ const want = personal ?? (recorded ? !!recorded.personal : !existsSync7(join8(root, MANIFEST_FILE)));
1473
+ if (!want || personalPinsFile(root)) return want;
1474
+ return personal ? "only the top folder of a git repo can have personal pins" : false;
1475
+ }
1476
+ var personalOf = (manifest) => Object.fromEntries(Object.entries(manifest.skills).filter(([, d]) => d.personal));
1314
1477
  function removeSkill(root, name, { force = false } = {}) {
1315
1478
  const manifest = readManifest(root);
1316
1479
  const recorded = manifest.skills[name];
@@ -1352,13 +1515,14 @@ function removeSkill(root, name, { force = false } = {}) {
1352
1515
  };
1353
1516
  unlinkOwn();
1354
1517
  const backedUp = local !== null && !inLibrary ? stash(dir, "edit-backup") : void 0;
1355
- if (!backedUp) rmSync3(dir, { recursive: true, force: true });
1518
+ if (!backedUp) rmSync4(dir, { recursive: true, force: true });
1356
1519
  delete manifest.skills[name];
1357
1520
  writeManifest(root, manifest);
1358
1521
  const left = [...leftLinks, ...takenLinks];
1359
1522
  return { name, action: "removed", ...left.length ? { reason: left.join("; ") } : {}, ...backedUp ? { backedUp } : {} };
1360
1523
  }
1361
1524
  function syncProject(root, { force = false, names } = {}) {
1525
+ excludePersonal(root);
1362
1526
  return Object.keys(readManifest(root).skills).filter((name) => !names || names.includes(name)).flatMap((name) => syncSkill(root, name, { force }));
1363
1527
  }
1364
1528
  function syncSkill(root, name, { force = false } = {}) {
@@ -1374,7 +1538,7 @@ function syncSkill(root, name, { force = false } = {}) {
1374
1538
  if (typeof version === "string") return [{ name, action: "skipped", reason: version }];
1375
1539
  if (local !== null && local !== dep.hash && versionForHash(name, local) === null && !force) return [{ name, action: "skipped", reason: "has local edits (use --force to reset them)" }];
1376
1540
  const backedUp = local !== null && versionForHash(name, local) === null ? stash(copy, "edit-backup") : void 0;
1377
- mkdirSync4(dirname3(copy), { recursive: true });
1541
+ mkdirSync5(dirname3(copy), { recursive: true });
1378
1542
  copySkill(versionDir(name, version), copy);
1379
1543
  const links = relinkPinned(root, name, dep);
1380
1544
  const from = (local !== null ? versionForHash(name, local)?.version : void 0) ?? version;
@@ -1396,7 +1560,7 @@ function relinkPinned(root, name, dep) {
1396
1560
  if (realpathOrNull(link2) === realpathOrNull(to)) continue;
1397
1561
  if (isLink(link2)) unlinkSync(link2);
1398
1562
  else if (entryExists(link2)) return { name, action: "skipped", reason: `${dir}/${name} is in the way of its link: move it, then sync` };
1399
- mkdirSync4(join8(root, dir), { recursive: true });
1563
+ mkdirSync5(join8(root, dir), { recursive: true });
1400
1564
  linkDir(to, link2);
1401
1565
  }
1402
1566
  return { name, action: "relinked", to: versionForHash(name, dep.hash)?.version ?? dep.version };
@@ -1426,13 +1590,13 @@ function updateProject(root, names, { force = false, allowTracked = false } = {}
1426
1590
  });
1427
1591
  }
1428
1592
  function importSkill(dir, { force = false } = {}) {
1429
- const name = basename3(dir);
1593
+ const name = basename4(dir);
1430
1594
  const to = librarySkillDir(name);
1431
1595
  const existing = treeHash(to);
1432
1596
  const incoming = treeHash(dir);
1433
1597
  if (existing === incoming) return { name, status: "unchanged" };
1434
1598
  if (existing !== null && !force) return { name, status: "exists" };
1435
- mkdirSync4(libraryDir(), { recursive: true });
1599
+ mkdirSync5(libraryDir(), { recursive: true });
1436
1600
  const origin = existing !== null && libraryOrigins()[name] ? null : originFor(dir);
1437
1601
  if (existing !== null) {
1438
1602
  forgetLatest(name);
@@ -1452,21 +1616,21 @@ function readBackupIndex(bucket) {
1452
1616
  }
1453
1617
  function move(from, to) {
1454
1618
  try {
1455
- renameSync2(from, to);
1619
+ renameSync3(from, to);
1456
1620
  } catch (err) {
1457
1621
  if (err.code !== "EXDEV") throw err;
1458
1622
  cpSync2(from, to, { recursive: true, verbatimSymlinks: true });
1459
- rmSync3(from, { recursive: true, force: true });
1623
+ rmSync4(from, { recursive: true, force: true });
1460
1624
  }
1461
1625
  }
1462
1626
  function stash(dir, bucket) {
1463
1627
  let at = Date.now();
1464
- const entryAt = (ms) => `${basename3(dir)}-${new Date(ms).toISOString().replace(/[:.]/g, "-")}`;
1628
+ const entryAt = (ms) => `${basename4(dir)}-${new Date(ms).toISOString().replace(/[:.]/g, "-")}`;
1465
1629
  while (entryExists(join8(skilllibHome(), bucket, entryAt(at)))) at++;
1466
1630
  const entry = entryAt(at);
1467
1631
  const target = join8(skilllibHome(), bucket, entry);
1468
1632
  const index = readBackupIndex(bucket);
1469
- mkdirSync4(join8(skilllibHome(), bucket), { recursive: true });
1633
+ mkdirSync5(join8(skilllibHome(), bucket), { recursive: true });
1470
1634
  move(dir, target);
1471
1635
  writeState(backupIndexFile(bucket), { ...index, [entry]: dir });
1472
1636
  return target;
@@ -1483,7 +1647,7 @@ function projectsUsing(name) {
1483
1647
  }
1484
1648
  function deleteLibrarySkill(name) {
1485
1649
  const using = projectsUsing(name);
1486
- if (using.length > 0) return { ok: false, reason: `still installed in ${using.map((p) => basename3(p)).join(", ")}` };
1650
+ if (using.length > 0) return { ok: false, reason: `still installed in ${using.map((p) => basename4(p)).join(", ")}` };
1487
1651
  const dir = librarySkillDir(name);
1488
1652
  if (!existsSync7(dir)) return { ok: false, reason: "not in the library" };
1489
1653
  forgetLatest(name);
@@ -1493,7 +1657,7 @@ function deleteGlobal(path, links = []) {
1493
1657
  return unloadGlobal(path, links, { requireLibrary: false });
1494
1658
  }
1495
1659
  function unloadGlobal(path, links = [], { requireLibrary = true } = {}) {
1496
- const name = basename3(path);
1660
+ const name = basename4(path);
1497
1661
  if (!entryExists(path) || name === "synced") return { ok: false, reason: "not a global skill" };
1498
1662
  const broken = !existsSync7(join8(path, "SKILL.md"));
1499
1663
  if (requireLibrary && !broken && latestVersion(name) === null) return { ok: false, reason: "import it into the library first" };
@@ -1503,7 +1667,7 @@ function unloadGlobal(path, links = [], { requireLibrary = true } = {}) {
1503
1667
  return { ok: true, movedTo };
1504
1668
  }
1505
1669
  function linkGlobal(path, agents) {
1506
- const name = basename3(path);
1670
+ const name = basename4(path);
1507
1671
  const linked = [];
1508
1672
  const skipped = [];
1509
1673
  for (const dir of new Set(agents.map((id) => harness(id).globalDirs()[0]))) {
@@ -1513,7 +1677,7 @@ function linkGlobal(path, agents) {
1513
1677
  if (realpathOrNull(link2) !== realpathOrNull(path)) skipped.push(link2);
1514
1678
  continue;
1515
1679
  }
1516
- mkdirSync4(dir, { recursive: true });
1680
+ mkdirSync5(dir, { recursive: true });
1517
1681
  linkDir(realpathOrNull(path) ?? path, join8(realpathOrNull(dir) ?? dir, name));
1518
1682
  linked.push(link2);
1519
1683
  }
@@ -1523,8 +1687,8 @@ function createSkill(name, description) {
1523
1687
  if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) return { ok: false, reason: "use lowercase letters, digits and dashes" };
1524
1688
  const dir = librarySkillDir(name);
1525
1689
  if (existsSync7(dir)) return { ok: false, reason: `${name} already exists` };
1526
- mkdirSync4(dir, { recursive: true });
1527
- writeFileSync2(
1690
+ mkdirSync5(dir, { recursive: true });
1691
+ writeFileSync3(
1528
1692
  join8(dir, "SKILL.md"),
1529
1693
  // JSON strings are valid YAML: a description with ": " or "#" stays one value.
1530
1694
  `---
@@ -1545,7 +1709,7 @@ function listBackups() {
1545
1709
  const dir = join8(skilllibHome(), kind);
1546
1710
  if (!existsSync7(dir)) return [];
1547
1711
  const index = readBackupIndex(kind);
1548
- return readdirSync4(dir).flatMap((entry) => {
1712
+ return readdirSync5(dir).flatMap((entry) => {
1549
1713
  const match = entry.match(/^(.*)-(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-\d{3}Z$/);
1550
1714
  if (!match) return [];
1551
1715
  const name = match[1];
@@ -1568,9 +1732,9 @@ function restoreBackup(backup) {
1568
1732
  const to = backup.from;
1569
1733
  if (!to) return { ok: false, reason: `skilllib doesn't know where it came from; its files are in ${tildify(backup.path)}` };
1570
1734
  if (backup.kind === "tidy-backup" && isLink(to)) unlinkSync(to);
1571
- if (backup.kind === "edit-backup" && !isLink(to) && versionForHash(backup.name, treeHash(to) ?? "") !== null) rmSync3(to, { recursive: true, force: true });
1735
+ if (backup.kind === "edit-backup" && !isLink(to) && versionForHash(backup.name, treeHash(to) ?? "") !== null) rmSync4(to, { recursive: true, force: true });
1572
1736
  if (entryExists(to)) return { ok: false, reason: `${backup.name} already exists there` };
1573
- mkdirSync4(join8(to, ".."), { recursive: true });
1737
+ mkdirSync5(join8(to, ".."), { recursive: true });
1574
1738
  move(backup.path, to);
1575
1739
  forgetLatest(backup.name);
1576
1740
  return { ok: true, to };
@@ -1606,13 +1770,13 @@ function installAgentSkill(enabled = enabledHarnesses()) {
1606
1770
  const foreign = [primary, ...links].find((d) => entryExists(d) && !isOurs(d) && !isDangling(d));
1607
1771
  if (foreign) return { ok: false, reason: `${tildify(foreign)} already exists and isn't skilllib's` };
1608
1772
  for (const d of [primary, ...links]) if (isDangling(d)) unlinkSync2(d);
1609
- mkdirSync5(primary, { recursive: true });
1610
- writeFileSync3(join9(primary, "SKILL.md"), shippedSkill());
1611
- writeFileSync3(join9(primary, OWN_SKILL_MARKER), "Installed by skilllib. `skilllib agent-skill remove` removes it.\n");
1773
+ mkdirSync6(primary, { recursive: true });
1774
+ writeFileSync4(join9(primary, "SKILL.md"), shippedSkill());
1775
+ writeFileSync4(join9(primary, OWN_SKILL_MARKER), "Installed by skilllib. `skilllib agent-skill remove` removes it.\n");
1612
1776
  for (const link2 of links) {
1613
- if (entryExists(link2) && !isLink(link2) && isOurs(link2)) rmSync4(link2, { recursive: true, force: true });
1777
+ if (entryExists(link2) && !isLink(link2) && isOurs(link2)) rmSync5(link2, { recursive: true, force: true });
1614
1778
  if (entryExists(link2)) continue;
1615
- mkdirSync5(join9(link2, ".."), { recursive: true });
1779
+ mkdirSync6(join9(link2, ".."), { recursive: true });
1616
1780
  linkDir(primary, link2);
1617
1781
  }
1618
1782
  return { ok: true, dirs: [primary, ...links] };
@@ -1623,7 +1787,7 @@ function removeAgentSkill() {
1623
1787
  for (const d of removed) {
1624
1788
  if (!entryExists(d)) continue;
1625
1789
  if (lstatSync3(d).isSymbolicLink()) unlinkSync2(d);
1626
- else rmSync4(d, { recursive: true, force: true });
1790
+ else rmSync5(d, { recursive: true, force: true });
1627
1791
  }
1628
1792
  return removed;
1629
1793
  }
@@ -1637,18 +1801,18 @@ function refreshAgentSkill() {
1637
1801
  // src/commands.ts
1638
1802
  import { existsSync as existsSync14, readFileSync as readFileSync7 } from "fs";
1639
1803
  import { parseArgs as parseFlags } from "util";
1640
- import { basename as basename9, join as join15, resolve as resolve8, sep as sep8 } from "path";
1804
+ import { basename as basename10, join as join15, resolve as resolve8, sep as sep8 } from "path";
1641
1805
 
1642
1806
  // src/git.ts
1643
- import { execFileSync as execFileSync2 } from "child_process";
1644
- import { existsSync as existsSync9, realpathSync as realpathSync4 } from "fs";
1807
+ import { execFileSync as execFileSync3 } from "child_process";
1808
+ import { existsSync as existsSync9, realpathSync as realpathSync3 } from "fs";
1645
1809
  import { isAbsolute as isAbsolute2, join as join10, relative as relative3, sep as sep5 } from "path";
1646
1810
  function isShared(state) {
1647
1811
  return state === "committed" || state === "changed";
1648
1812
  }
1649
1813
  function git(root, args, input) {
1650
1814
  try {
1651
- return execFileSync2("git", args, { cwd: root, encoding: "utf-8", input, stdio: ["pipe", "pipe", "ignore"], maxBuffer: 16 * 1024 * 1024 });
1815
+ return execFileSync3("git", args, { cwd: root, encoding: "utf-8", input, stdio: ["pipe", "pipe", "ignore"], maxBuffer: 16 * 1024 * 1024 });
1652
1816
  } catch (err) {
1653
1817
  const out2 = err.stdout;
1654
1818
  return typeof out2 === "string" ? out2 : null;
@@ -1657,14 +1821,14 @@ function git(root, args, input) {
1657
1821
  function gitInfo(root, extraDirs = []) {
1658
1822
  let inRepo;
1659
1823
  try {
1660
- inRepo = execFileSync2("git", ["rev-parse", "--show-prefix"], { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1824
+ inRepo = execFileSync3("git", ["rev-parse", "--show-prefix"], { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1661
1825
  } catch {
1662
1826
  if (!existsSync9(join10(root, ".git"))) return null;
1663
1827
  inRepo = "";
1664
1828
  }
1665
1829
  const real = (p) => {
1666
1830
  try {
1667
- return realpathSync4.native(p);
1831
+ return realpathSync3.native(p);
1668
1832
  } catch {
1669
1833
  return p;
1670
1834
  }
@@ -1703,23 +1867,23 @@ function relativeTo(root, path) {
1703
1867
  }
1704
1868
 
1705
1869
  // src/here.ts
1706
- import { readdirSync as readdirSync8 } from "fs";
1707
- import { basename as basename7 } from "path";
1870
+ import { readdirSync as readdirSync9 } from "fs";
1871
+ import { basename as basename8 } from "path";
1708
1872
 
1709
1873
  // src/health.ts
1710
- import { existsSync as existsSync12, lstatSync as lstatSync4, readdirSync as readdirSync7 } from "fs";
1711
- import { basename as basename6, dirname as dirname6, isAbsolute as isAbsolute3, join as join13, relative as relative4, sep as sep7 } from "path";
1874
+ import { existsSync as existsSync12, lstatSync as lstatSync4, readdirSync as readdirSync8 } from "fs";
1875
+ import { basename as basename7, dirname as dirname6, isAbsolute as isAbsolute3, join as join13, relative as relative4, sep as sep7 } from "path";
1712
1876
 
1713
1877
  // src/plugins.ts
1714
- import { execFileSync as execFileSync3 } from "child_process";
1715
- import { existsSync as existsSync10, mkdirSync as mkdirSync6, readdirSync as readdirSync5, readFileSync as readFileSync6, statSync as statSync4, writeFileSync as writeFileSync4 } from "fs";
1716
- import { basename as basename4, dirname as dirname4, join as join11, resolve as resolve7 } from "path";
1878
+ import { execFileSync as execFileSync4 } from "child_process";
1879
+ import { existsSync as existsSync10, mkdirSync as mkdirSync7, readdirSync as readdirSync6, readFileSync as readFileSync6, statSync as statSync5, writeFileSync as writeFileSync5 } from "fs";
1880
+ import { basename as basename5, dirname as dirname4, join as join11, resolve as resolve7 } from "path";
1717
1881
  function pluginNameOf(id) {
1718
1882
  return id.split("@")[0];
1719
1883
  }
1720
1884
  function nonEmptyDir(path) {
1721
1885
  try {
1722
- return readdirSync5(path).some((f) => !f.startsWith("."));
1886
+ return readdirSync6(path).some((f) => !f.startsWith("."));
1723
1887
  } catch {
1724
1888
  return false;
1725
1889
  }
@@ -1810,23 +1974,23 @@ function installedPlugins() {
1810
1974
  return root ? [{ id, scope: "user", synced: false, agent: "claude-code", on: true, settingsOnly: true, root, ...describe(root) }] : [];
1811
1975
  });
1812
1976
  const syncedDir = join11(plugins, "synced");
1813
- const dirs = (p) => existsSync10(p) ? readdirSync5(p, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".")).map((d) => join11(p, d.name)) : [];
1977
+ const dirs = (p) => existsSync10(p) ? readdirSync6(p, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".")).map((d) => join11(p, d.name)) : [];
1814
1978
  const fromSynced = dirs(syncedDir).flatMap(dirs).map((root) => {
1815
- const id = `${readJson(join11(root, ".claude-plugin", "plugin.json"))?.name ?? basename4(root)}@synced`;
1979
+ const id = `${readJson(join11(root, ".claude-plugin", "plugin.json"))?.name ?? basename5(root)}@synced`;
1816
1980
  return { id, scope: null, synced: true, agent: "claude-code", on: user[id] !== false, root, ...describe(root) };
1817
1981
  });
1818
1982
  return [...fromMarketplaces, ...fromSettings, ...fromSynced];
1819
1983
  }
1820
1984
  function cursorPlugins() {
1821
1985
  const cache = join11(userHome(), ".cursor", "plugins", "cache");
1822
- const dirs = (p) => existsSync10(p) ? readdirSync5(p).filter((d) => !d.startsWith(".") && statSync4(join11(p, d)).isDirectory()) : [];
1986
+ const dirs = (p) => existsSync10(p) ? readdirSync6(p).filter((d) => !d.startsWith(".") && statSync5(join11(p, d)).isDirectory()) : [];
1823
1987
  return dirs(cache).flatMap(
1824
1988
  (marketplace) => dirs(join11(cache, marketplace)).flatMap((plugin) => {
1825
1989
  const versions = dirs(join11(cache, marketplace, plugin)).map((v) => join11(cache, marketplace, plugin, v));
1826
- const root = versions.sort((a, b) => statSync4(b).mtimeMs - statSync4(a).mtimeMs)[0];
1990
+ const root = versions.sort((a, b) => statSync5(b).mtimeMs - statSync5(a).mtimeMs)[0];
1827
1991
  if (!root) return [];
1828
1992
  const about = describe(root);
1829
- return [{ id: `${plugin}@${marketplace}`, scope: null, synced: false, agent: "cursor", on: null, root, ...about, version: about.version ?? basename4(root) }];
1993
+ return [{ id: `${plugin}@${marketplace}`, scope: null, synced: false, agent: "cursor", on: null, root, ...about, version: about.version ?? basename5(root) }];
1830
1994
  })
1831
1995
  );
1832
1996
  }
@@ -1840,7 +2004,7 @@ function claudeBinary() {
1840
2004
  const candidates = ["claude", join11(userHome(), ".local", "bin", "claude"), join11(userHome(), ".claude", "local", "claude")];
1841
2005
  const found = candidates.find((bin) => {
1842
2006
  try {
1843
- return execFileSync3(bin, ["--version"], { stdio: ["ignore", "pipe", "ignore"], timeout: 1e4, encoding: "utf-8" }).includes("Claude Code");
2007
+ return execFileSync4(bin, ["--version"], { stdio: ["ignore", "pipe", "ignore"], timeout: 1e4, encoding: "utf-8" }).includes("Claude Code");
1844
2008
  } catch {
1845
2009
  return false;
1846
2010
  }
@@ -1854,7 +2018,7 @@ function claude(args, cwd, bin = claudeBinary()) {
1854
2018
  if (!bin) return { ok: false, reason: `Claude Code's CLI didn't run (tried \`claude\` on your PATH and ~/.local/bin/claude); run \`claude ${args.join(" ")}\` yourself` };
1855
2019
  const slow = args[1] === "install" || args[1] === "update";
1856
2020
  try {
1857
- const out2 = execFileSync3(bin, args, { cwd, stdio: ["ignore", "pipe", "pipe"], timeout: slow ? 12e4 : 2e4, encoding: "utf-8" });
2021
+ const out2 = execFileSync4(bin, args, { cwd, stdio: ["ignore", "pipe", "pipe"], timeout: slow ? 12e4 : 2e4, encoding: "utf-8" });
1858
2022
  return { ok: true, out: out2 };
1859
2023
  } catch (err) {
1860
2024
  const e = err;
@@ -1870,7 +2034,7 @@ function setPluginIn(file, id, on) {
1870
2034
  }
1871
2035
  function ignoreLocalSettings(root) {
1872
2036
  const file = ".claude/settings.local.json";
1873
- const git2 = (...args) => execFileSync3("git", args, { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
2037
+ const git2 = (...args) => execFileSync4("git", args, { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1874
2038
  try {
1875
2039
  git2("rev-parse", "--git-dir");
1876
2040
  } catch {
@@ -1889,8 +2053,8 @@ function ignoreLocalSettings(root) {
1889
2053
  try {
1890
2054
  const exclude = resolve7(root, git2("rev-parse", "--git-path", "info/exclude"));
1891
2055
  const before = existsSync10(exclude) ? readFileSync6(exclude, "utf-8") : "";
1892
- mkdirSync6(dirname4(exclude), { recursive: true });
1893
- writeFileSync4(exclude, `${before}${before && !before.endsWith("\n") ? "\n" : ""}/${git2("rev-parse", "--show-prefix")}${file}
2056
+ mkdirSync7(dirname4(exclude), { recursive: true });
2057
+ writeFileSync5(exclude, `${before}${before && !before.endsWith("\n") ? "\n" : ""}/${git2("rev-parse", "--show-prefix")}${file}
1894
2058
  `);
1895
2059
  return ", which git now ignores via .git/info/exclude";
1896
2060
  } catch {
@@ -1898,7 +2062,7 @@ function ignoreLocalSettings(root) {
1898
2062
  }
1899
2063
  }
1900
2064
  function turnOffIn(plugin, root) {
1901
- const done = `${plugin.id} turned off in ${basename4(root)}; other repos keep it`;
2065
+ const done = `${plugin.id} turned off in ${basename5(root)}; other repos keep it`;
1902
2066
  const bin = plugin.synced || plugin.settingsOnly ? null : claudeBinary();
1903
2067
  if (!bin) {
1904
2068
  const r2 = setPluginIn(join11(root, ".claude", "settings.local.json"), plugin.id, false);
@@ -1917,7 +2081,7 @@ function settingsFileOf(p) {
1917
2081
  return join11(p.projectPath, ".claude", p.scope === "local" ? "settings.local.json" : "settings.json");
1918
2082
  }
1919
2083
  function whereOf(p) {
1920
- return p.projectPath ? ` in ${basename4(p.projectPath)} (${p.scope})` : "";
2084
+ return p.projectPath ? ` in ${basename5(p.projectPath)} (${p.scope})` : "";
1921
2085
  }
1922
2086
  function setPluginOn(p, on) {
1923
2087
  const shared = p.scope === "project" && p.projectPath ? "; it's in .claude/settings.json, which git tracks, so commit it if the team should get it" : "";
@@ -1969,17 +2133,17 @@ function removePlugin(plugin, how) {
1969
2133
  const uninstall = how === "delete" && !plugin.synced;
1970
2134
  if (uninstall && !claudeBinary()) return { ok: false, message: `${plugin.id}: nothing changed; Claude Code's CLI didn't run (tried \`claude\` on your PATH and ~/.local/bin/claude)` };
1971
2135
  const yours = new Set(machineSkills().filter((m) => m.movable).map((m) => m.name));
1972
- const imported = plugin.skills.filter((dir) => !yours.has(basename4(dir)) && latestVersion(basename4(dir)) === null).map((dir) => importSkill(dir).name);
2136
+ const imported = plugin.skills.filter((dir) => !yours.has(basename5(dir)) && latestVersion(basename5(dir)) === null).map((dir) => importSkill(dir).name);
1973
2137
  const saved = imported.length ? `; ${imported.join(", ")} copied into Your skills` : "";
1974
2138
  const r = uninstall ? uninstallPlugin(plugin) : setPluginOn(plugin, false);
1975
2139
  if (!r.ok) return { ok: false, message: `${r.message}${saved}` };
1976
2140
  const others = uninstall ? (installedPluginsJson()[plugin.id] ?? []).filter((e) => (e.scope ?? "user") !== (plugin.scope ?? "user") || e.projectPath !== plugin.projectPath) : [];
1977
- const left = others.length ? `; its ${others.map((e) => `${e.scope ?? "user"} install${e.projectPath ? ` in ${basename4(e.projectPath)}` : ""}`).join(", ")} stays` : "";
2141
+ const left = others.length ? `; its ${others.map((e) => `${e.scope ?? "user"} install${e.projectPath ? ` in ${basename5(e.projectPath)}` : ""}`).join(", ")} stays` : "";
1978
2142
  return { ok: true, message: uninstall ? `${plugin.id} removed (${plugin.scope ?? "user"} install)${saved}${left}` : `${r.message}${saved}` };
1979
2143
  }
1980
2144
  function recordRemovedPlugin(id, scope, projectPath, on = true) {
1981
2145
  const list2 = readState(removedFile()) ?? [];
1982
- mkdirSync6(skilllibHome(), { recursive: true });
2146
+ mkdirSync7(skilllibHome(), { recursive: true });
1983
2147
  writeState(removedFile(), [...list2, { id, scope, ...projectPath ? { projectPath } : {}, on, at: (/* @__PURE__ */ new Date()).toISOString() }]);
1984
2148
  }
1985
2149
  function pluginBackups() {
@@ -2002,16 +2166,16 @@ function restorePlugin(backup) {
2002
2166
  const wasOff = records.filter(same).pop()?.on === false;
2003
2167
  const off = wasOff ? claude(["plugin", "disable", backup.from, "--scope", scope], projectPath) : null;
2004
2168
  writeState(removedFile(), records.filter((x) => !same(x)));
2005
- const where = `Claude Code plugins (${scope}${projectPath ? ` in ${basename4(projectPath)}` : ""}${wasOff ? off?.ok ? ", off as it was" : ", but it's on: turn it off from Plugins" : ""})`;
2169
+ const where = `Claude Code plugins (${scope}${projectPath ? ` in ${basename5(projectPath)}` : ""}${wasOff ? off?.ok ? ", off as it was" : ", but it's on: turn it off from Plugins" : ""})`;
2006
2170
  return { ok: true, to: where };
2007
2171
  }
2008
2172
  function cursorPluginSkills(plugins = cursorPlugins()) {
2009
- return plugins.flatMap((p) => p.skills.map((path) => ({ name: basename4(path), plugin: pluginNameOf(p.id), path })));
2173
+ return plugins.flatMap((p) => p.skills.map((path) => ({ name: basename5(path), plugin: pluginNameOf(p.id), path })));
2010
2174
  }
2011
2175
 
2012
2176
  // src/tidy.ts
2013
- import { existsSync as existsSync11, mkdirSync as mkdirSync7, readdirSync as readdirSync6, unlinkSync as unlinkSync3 } from "fs";
2014
- import { basename as basename5, dirname as dirname5, join as join12, sep as sep6 } from "path";
2177
+ import { existsSync as existsSync11, mkdirSync as mkdirSync8, readdirSync as readdirSync7, unlinkSync as unlinkSync3 } from "fs";
2178
+ import { basename as basename6, dirname as dirname5, join as join12, sep as sep6 } from "path";
2015
2179
  function entriesOf(dirs, name) {
2016
2180
  return dirs.flatMap((dir) => {
2017
2181
  const path = join12(dir, name);
@@ -2033,7 +2197,7 @@ function namesIn(dirs) {
2033
2197
  const names = /* @__PURE__ */ new Set();
2034
2198
  for (const dir of dirs) {
2035
2199
  if (!isDir(dir)) continue;
2036
- for (const name of readdirSync6(dir)) if (!name.startsWith(".") && name !== "synced") names.add(name);
2200
+ for (const name of readdirSync7(dir)) if (!name.startsWith(".") && name !== "synced") names.add(name);
2037
2201
  }
2038
2202
  return [...names].sort();
2039
2203
  }
@@ -2046,7 +2210,7 @@ function globalTidyDirs() {
2046
2210
  function runsFrom(id, dirsWithCopy, readsOf) {
2047
2211
  const reads = readsOf(id).filter((d) => dirsWithCopy.includes(d));
2048
2212
  if (id === "codex") return reads;
2049
- const pick = (d) => CURSOR_PICKS.indexOf(basename5(dirname5(d)));
2213
+ const pick = (d) => CURSOR_PICKS.indexOf(basename6(dirname5(d)));
2050
2214
  return id === "cursor" ? [...reads].sort((a, b) => pick(a) - pick(b)).slice(0, 1) : reads.slice(0, 1);
2051
2215
  }
2052
2216
  function conflictOf(name, root, reals, enabled, readsOf) {
@@ -2157,7 +2321,7 @@ function gitVisibleSteps(plan, git2 = plan.root ? gitInfo(plan.root) : null) {
2157
2321
  if (!plan.root || !git2) return [];
2158
2322
  const shared = (rel) => isShared(git2.of(rel));
2159
2323
  const dep = readManifest(plan.root).skills[plan.name];
2160
- const recordsLink = (s) => !!dep && (s.kind === "link" || s.kind === "replace") && !dep.links?.includes(relativeTo(plan.root, dirname5(s.path)));
2324
+ const recordsLink = (s) => !!dep && !dep.personal && (s.kind === "link" || s.kind === "replace") && !dep.links?.includes(relativeTo(plan.root, dirname5(s.path)));
2161
2325
  return plan.steps.filter((s) => shared(relativeTo(plan.root, s.path)) || recordsLink(s) && shared(MANIFEST_FILE));
2162
2326
  }
2163
2327
  function applyTidy(plan, { git: git2 = "keep" } = {}) {
@@ -2170,7 +2334,7 @@ function applyTidy(plan, { git: git2 = "keep" } = {}) {
2170
2334
  if (isLink(step.path)) unlinkSync3(step.path);
2171
2335
  } else stash(step.path, "tidy-backup");
2172
2336
  if (step.kind === "link" || step.kind === "replace") {
2173
- mkdirSync7(dirname5(step.path), { recursive: true });
2337
+ mkdirSync8(dirname5(step.path), { recursive: true });
2174
2338
  linkDir(step.target, step.path);
2175
2339
  }
2176
2340
  applied.push(step);
@@ -2233,7 +2397,7 @@ function track(root, name) {
2233
2397
  if (c.action === "skipped") throw new Error(`${name}: ${c.reason}`);
2234
2398
  return c;
2235
2399
  }
2236
- var installedIn = (p) => p.projectPath ? ` in ${basename6(p.projectPath)}` : "";
2400
+ var installedIn = (p) => p.projectPath ? ` in ${basename7(p.projectPath)}` : "";
2237
2401
  var installId = (p) => `${p.projectPath ? `${p.projectPath}:` : ""}${p.id}`;
2238
2402
  function summarize(where, verb, changes) {
2239
2403
  const done = changes.filter((c) => c.action !== "skipped");
@@ -2255,7 +2419,7 @@ var gitNote = (root, paths) => {
2255
2419
  };
2256
2420
  var agentNames = (ids) => ids.map((id) => harness(id).name).join(", ");
2257
2421
  function conflictIssue(c) {
2258
- const where = c.root ? basename6(c.root) : "your global folders";
2422
+ const where = c.root ? basename7(c.root) : "your global folders";
2259
2423
  const winners = c.managed ? c.copies.filter((x) => x.dir === c.managed) : c.copies;
2260
2424
  const planFor = (dir) => (c.root ? planProjectTidy(c.root, { keep: { [c.name]: dir } }) : planGlobalTidy({ keep: { [c.name]: dir } })).plans.find((p) => p.name === c.name);
2261
2425
  const keep = (dir, git2) => () => {
@@ -2289,7 +2453,7 @@ function applyAll(plans, git2) {
2289
2453
  return { skills: results.filter((r) => r.applied.length).length, held: results.reduce((n, r) => n + r.held.length, 0) };
2290
2454
  }
2291
2455
  function brokenLinksIn(root, opened) {
2292
- const where = basename6(root);
2456
+ const where = basename7(root);
2293
2457
  const tidyDirs = repoTidyDirs(root);
2294
2458
  const hasCopy = (name) => tidyDirs.some((d) => !isLink(join13(root, d, name)) && existsSync12(join13(root, d, name, "SKILL.md")));
2295
2459
  const mends = (project, dir, name) => {
@@ -2321,7 +2485,7 @@ function brokenLinksIn(root, opened) {
2321
2485
  return folders2.flatMap(({ dir, top, own, real }) => {
2322
2486
  const folder = join13(root, dir);
2323
2487
  if (!isDir(folder) || notWritable(root, dir)) return [];
2324
- return readdirSync7(folder).flatMap((name) => {
2488
+ return readdirSync8(folder).flatMap((name) => {
2325
2489
  const path = join13(folder, name);
2326
2490
  if (!isLink(path) || isDir(path) || top && (dir !== GROK_SKILLS_DIR && hasCopy(name) || syncMends(dir, name))) return [];
2327
2491
  const remove2 = {
@@ -2360,24 +2524,24 @@ function grokDiffers(projects2, machine, statusOf) {
2360
2524
  const issue = (root, name, grokCopy, other) => ({
2361
2525
  id: `grok-differs:${root ?? "global"}:${name}`,
2362
2526
  severity: "suggestion",
2363
- title: `${name}${root ? ` in ${basename6(root)}` : ""}: Grok runs a different copy`,
2527
+ title: `${name}${root ? ` in ${basename7(root)}` : ""}: Grok runs a different copy`,
2364
2528
  detail: `Grok reads ${grokCopy} first; your other agents run ${other}, and the two differ. skilllib doesn't change Grok's folder: update or remove that copy there to make them match.`,
2365
2529
  at: rows(root, [name])
2366
2530
  });
2367
2531
  const grokGlobal = join13(grokDir(), "skills");
2368
2532
  const global = skillDirsIn(grokGlobal).filter((path) => !isLink(path)).flatMap((path) => {
2369
- const name = basename6(path);
2533
+ const name = basename7(path);
2370
2534
  const other = machine.find((m) => m.name === name && m.movable && !m.broken && m.harnesses.some((id) => id !== "grok") && realPath(dirname6(m.path)) !== realPath(grokGlobal));
2371
2535
  return other && treeHash(other.path) !== treeHash(path) ? [issue(null, name, tildify(path), tildify(other.path))] : [];
2372
2536
  });
2373
- const repos = projects2.flatMap(
2537
+ const repos2 = projects2.flatMap(
2374
2538
  (root) => skillDirsIn(join13(root, GROK_SKILLS_DIR)).filter((path) => !isLink(path)).flatMap((path) => {
2375
- const name = basename6(path);
2539
+ const name = basename7(path);
2376
2540
  const other = statusOf(root).find((s) => s.name === name && !s.path.startsWith(join13(root, GROK_SKILLS_DIR)));
2377
2541
  return other && treeHash(other.path) !== treeHash(path) ? [issue(root, name, `${GROK_SKILLS_DIR}/${name}`, relativeTo(root, other.path))] : [];
2378
2542
  })
2379
2543
  );
2380
- return [...global, ...repos];
2544
+ return [...global, ...repos2];
2381
2545
  }
2382
2546
  function findIssues(roots, machine, libraryNames, statusOf = projectStatus, loadedIn = (root) => skillsLoadedIn(root, machine), nestedOf = nestedSkills) {
2383
2547
  gitMemo = /* @__PURE__ */ new Map();
@@ -2385,8 +2549,8 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
2385
2549
  const issues = unreadable.map(({ root, reason }) => ({
2386
2550
  id: `manifest:${root}`,
2387
2551
  severity: "problem",
2388
- title: `${basename6(root)}: skilllib.json can't be read`,
2389
- detail: `${reason}. Until it's fixed, skilllib leaves ${basename6(root)} out.`,
2552
+ title: `${basename7(root)}: skilllib.json can't be read`,
2553
+ detail: `${reason}. Until it's fixed, skilllib leaves ${basename7(root)} out.`,
2390
2554
  at: [{ root, skill: "" }]
2391
2555
  }));
2392
2556
  const statuses = new Map(projects2.map((root) => [root, statusOf(root)]));
@@ -2437,7 +2601,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
2437
2601
  const globalYours = new Set(loaded.filter((m) => m.movable).map((m) => m.name));
2438
2602
  const installedYours = /* @__PURE__ */ new Set([...globalYours, ...[...statuses.values()].flat().map((s) => s.name), ...knownProjects().flatMap((p) => Object.keys(pinsIn(p)))]);
2439
2603
  const repoChoices = (plugin, root, names) => {
2440
- const where = basename6(root);
2604
+ const where = basename7(root);
2441
2605
  const copies = statuses.get(root).filter((s) => names.includes(s.name));
2442
2606
  const allManaged = names.every((n) => copies.some((c) => c.name === n && removesClean(root, c)));
2443
2607
  const committed = gitNote(root, copies.map((s) => s.path)) !== "";
@@ -2463,7 +2627,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
2463
2627
  };
2464
2628
  const machinePlugins = claudePlugins(machine);
2465
2629
  for (const plugin of machinePlugins) {
2466
- const names = plugin.skills.map((p) => basename6(p));
2630
+ const names = plugin.skills.map((p) => basename7(p));
2467
2631
  const inRepos = projects2.filter((root) => pluginAgentsIn(plugin, root).has("claude-code")).map((root) => [root, namesIn2(root).filter((n) => names.includes(n))]).filter(([, d]) => d.length);
2468
2632
  const cursorRepos = enabledHarnesses().includes("claude-code") ? projects2.filter((root) => !pluginAgentsIn(plugin, root).has("claude-code") && pluginAgentsIn(plugin, root).has("cursor")).map((root) => [root, namesIn2(root, "cursor").filter((n) => names.includes(n))]).filter(([, d]) => d.length) : [];
2469
2633
  const claudeYours = /* @__PURE__ */ new Set([...globalYours, ...inRepos.flatMap(([, d]) => d)]);
@@ -2480,7 +2644,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
2480
2644
  const off = { label: `Turn the plugin ${plugin.id} off`, hint: "stays installed", run: () => orThrow(removePlugin(plugin, "off")) };
2481
2645
  const bothLoad = dupes.filter((n) => loadedYours.has(n));
2482
2646
  const claudeBoth = dupes.filter((n) => claudeYours.has(n));
2483
- const cursorWheres = cursorRepos.map(([root]) => basename6(root));
2647
+ const cursorWheres = cursorRepos.map(([root]) => basename7(root));
2484
2648
  const notInstalled = dupes.filter((n) => !installedYours.has(n));
2485
2649
  issues.push({
2486
2650
  id: `plugin:${plugin.id}`,
@@ -2494,7 +2658,7 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
2494
2658
  plugin.extras.length ? `It also brings ${plugin.extras.join(", ")}: ${plugin.synced || plugin.settingsOnly ? "turning it off pauses those" : "removing it drops those, turning it off pauses them"}.` : "",
2495
2659
  plugin.synced ? "It's synced from claude.ai, so it can only be turned off here." : "",
2496
2660
  plugin.settingsOnly ? "It's only turned on in your settings, not installed, so it can only be turned off here." : "",
2497
- inRepos.length ? `Or keep it and turn it off only in ${inRepos.map(([root]) => basename6(root)).join(", ")}.` : ""
2661
+ inRepos.length ? `Or keep it and turn it off only in ${inRepos.map(([root]) => basename7(root)).join(", ")}.` : ""
2498
2662
  ].filter(Boolean).join(" "),
2499
2663
  choices: [...plugin.synced || plugin.settingsOnly ? [off] : plugin.extras.length ? [off, remove2] : [remove2, off], ...inRepos.flatMap(([root, d]) => repoChoices(plugin, root, d))],
2500
2664
  // Global's rows (yours and the plugin's), and the repos where both load.
@@ -2517,10 +2681,10 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
2517
2681
  }
2518
2682
  const machinePluginSkills = new Set(machinePlugins.flatMap((p) => p.skills));
2519
2683
  for (const root of projects2) {
2520
- const where = basename6(root);
2684
+ const where = basename7(root);
2521
2685
  const repoOnly = loadedHere.get(root).filter((m) => m.kind === "plugin" && !machinePluginSkills.has(m.path));
2522
2686
  for (const plugin of enabledHarnesses().includes("claude-code") ? claudePlugins(repoOnly, root) : []) {
2523
- const names = plugin.skills.map((p) => basename6(p));
2687
+ const names = plugin.skills.map((p) => basename7(p));
2524
2688
  const inRepo = namesIn2(root).filter((n) => names.includes(n));
2525
2689
  const dupes = names.filter((n) => inRepo.includes(n) || globalYours.has(n));
2526
2690
  if (!dupes.length) continue;
@@ -2610,385 +2774,8 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
2610
2774
  const alsoGlobal = /* @__PURE__ */ new Map();
2611
2775
  const globalCopies = loaded.filter((m) => m.kind !== "plugin");
2612
2776
  const alsoLoadedGlobally = (s) => globalCopies.some((m) => m.name === s.name && m.harnesses.some((id) => s.visibility.some((v) => v.id === id && v.paths > 0)));
2613
- for (const root of projects2) {
2614
- const where = basename6(root);
2615
- const status2 = statuses.get(root);
2616
- issues.push(...brokenLinksIn(root, openedProjects));
2617
- const pins = readManifest(root).skills;
2618
- const blockedFor = /* @__PURE__ */ new Map();
2619
- const untrackable = (s) => {
2620
- if (!blockedFor.has(s.name)) blockedFor.set(s.name, trackBlocker(root, s.name));
2621
- return blockedFor.get(s.name);
2622
- };
2623
- const blockers = new Map(status2.filter((s) => s.managed).flatMap((s) => ((b) => b ? [[s.name, b]] : [])(syncBlocker(root, s.name, pins[s.name]))));
2624
- for (const [name, blocker] of blockers)
2625
- issues.push({
2626
- id: `stuck:${root}:${name}`,
2627
- severity: "problem",
2628
- title: `${name} in ${where}: sync can't put it back as skilllib.json pins it`,
2629
- // What to do depends on what's there: a folder that links out of the repo is a setup of yours, and a pin
2630
- // your library has no version of can't come back by sync anyway.
2631
- detail: `${blocker.said}, so sync can't put it back there.${blocker.outside ? " skilllib only installs inside the repo: point that folder back into the repo, or remove the skill here (skilllib remove)." : versionForHash(name, pins[name]?.hash ?? "") !== null ? " Move it out of the way, then run skilllib sync." : " Move it out of the way; your library has no copy of the pinned version either."}`,
2632
- at: rows(root, [name])
2633
- });
2634
- const unstuck = status2.filter((s) => !blockers.has(s.name));
2635
- const missing = unstuck.filter((s) => s.state === "folder missing" && s.latest !== null);
2636
- const knownPin = (s) => versionForHash(s.name, pins[s.name]?.hash ?? "") !== null;
2637
- const restorable = missing.filter(knownPin);
2638
- for (const s of unstuck.filter((m) => (m.state === "folder missing" || m.state === "out of sync") && m.latest !== null && !knownPin(m)))
2639
- issues.push({
2640
- id: `pin:${root}:${s.name}`,
2641
- severity: "problem",
2642
- title: `${s.name}: the version ${where}'s skilllib.json pins isn't in your library`,
2643
- detail: `${s.state === "folder missing" ? "The folder is missing, and no" : "The copy is another version, and no"} version in Your skills has the content it pins (pinned from another library?). ${untrackable(s) ? `${untrackable(s)}, so skilllib can't install it here.` : `Install your newest v${s.latest}; ${where}'s skilllib.json then pins it.`}`,
2644
- at: rows(root, [s.name]),
2645
- // It changes what the shared skilllib.json pins: a choice, never run by `doctor --fix`.
2646
- ...untrackable(s) ? {} : { choices: [{ label: `Install your newest (v${s.latest})`, hint: `${where}'s skilllib.json pins it`, run: () => `${s.name}: installed v${track(root, s.name).to}` }] }
2647
- });
2648
- if (restorable.length > 0) {
2649
- issues.push({
2650
- id: `sync:${root}`,
2651
- severity: "problem",
2652
- title: `${where}: ${plural(restorable.length, "skill")} in skilllib.json not installed`,
2653
- detail: `${restorable.map((s) => s.name).join(", ")}: the folder is missing`,
2654
- at: rows(root, restorable.map((s) => s.name)),
2655
- fix: {
2656
- label: "Sync the project",
2657
- // Only the missing folders: an out-of-sync copy (maybe committed) is its own issue's choice.
2658
- run: () => summarize(where, "restored", syncProject(root, { names: restorable.map((s) => s.name) }))
2659
- }
2660
- });
2661
- }
2662
- const drifted = unstuck.filter((s) => s.state === "out of sync" && knownPin(s));
2663
- if (drifted.length) {
2664
- const seen = drifted.some((s) => isShared(gitOf(root)?.of(relativeTo(root, s.path))));
2665
- const yours = drifted.some((s) => untrackable(s));
2666
- const install = {
2667
- label: "Install the pinned versions",
2668
- hint: seen ? "git will see the copies change" : "your library keeps every version",
2669
- run: () => summarize(where, "synced", syncProject(root, { names: drifted.map((s) => s.name) }))
2670
- };
2671
- issues.push({
2672
- id: `out-of-sync:${root}`,
2673
- severity: "suggestion",
2674
- title: `${where}: ${plural(drifted.length, "skill")} not at the version skilllib.json pins`,
2675
- detail: `${drifted.map((s) => s.name).join(", ")}: each copy is another version from your library. sync installs the pinned one.${yours ? ` ${[...new Set(drifted.flatMap((s) => untrackable(s) ? [untrackable(s)] : []))].map((why) => `${drifted.filter((s) => untrackable(s) === why).map((s) => s.name).join(", ")}: ${why}`).join("; ")}, so skilllib can't pin the version that's there; ${drifted.filter((s) => untrackable(s)).length === 1 ? "it may be edits you copied into Your skills on purpose, and stays" : "they may be edits you copied into Your skills on purpose, and stay"} unless you choose this.` : ""}`,
2676
- at: rows(root, drifted.map((s) => s.name)),
2677
- ...seen || yours ? { choices: [install] } : { fix: install }
2678
- });
2679
- }
2680
- for (const s of unstuck.filter((s2) => s2.state === "folder missing" && s2.latest === null))
2681
- issues.push({
2682
- id: `unavailable:${root}:${s.name}`,
2683
- severity: "problem",
2684
- title: `${s.name}: pinned in ${where}'s skilllib.json, but not in your library`,
2685
- detail: `Its folder is missing, and your library (${tildify(libraryDir())}) has no copy to restore: import it, or set SKILLLIB_HOME to the skilllib folder whose library has it.`,
2686
- at: rows(root, [s.name]),
2687
- // Editing skilllib.json is the team's call: a choice, never run by `doctor --fix`.
2688
- choices: [
2689
- {
2690
- label: "Remove it from skilllib.json",
2691
- hint: `${where} stops pinning it`,
2692
- run: () => {
2693
- const r = removeSkill(root, s.name);
2694
- if (r.action !== "removed") throw new Error(`${s.name}: ${r.reason}`);
2695
- return `${s.name}: removed from ${where}'s skilllib.json`;
2696
- }
2697
- }
2698
- ]
2699
- });
2700
- const behind = unstuck.filter((s) => s.state === "update available" && !untrackable(s));
2701
- if (behind.length > 0)
2702
- issues.push({
2703
- id: `update:${root}`,
2704
- severity: "suggestion",
2705
- title: `${where}: ${plural(behind.length, "skill")} out of date`,
2706
- detail: behind.map((s) => `${s.name} (v${s.version} \u2192 v${s.latest})`).join(", "),
2707
- at: rows(root, behind.map((s) => s.name)),
2708
- // It changes what the repo pins and the copies its agents load (often committed): a choice, never `doctor --fix`.
2709
- choices: [
2710
- {
2711
- label: "Update to the newest versions",
2712
- hint: isShared(gitOf(root)?.of(MANIFEST_FILE)) ? "git will see the new copies and skilllib.json" : "replaces each copy with your library's newest",
2713
- run: () => summarize(where, "updated", updateProject(root, behind.map((s) => s.name)))
2714
- }
2715
- ]
2716
- });
2717
- const tidy2 = planProjectTidy(root);
2718
- if (tidy2.plans.length) {
2719
- const git3 = gitOf(root);
2720
- const visible2 = tidy2.plans.flatMap((p) => gitVisibleSteps(p, git3).map((s) => describeStep(s, root)));
2721
- const run = (mode) => () => {
2722
- const r = applyAll(tidy2.plans, mode);
2723
- if (!r.skills) throw new Error(`${where}: nothing tidied${r.held ? `; left ${plural(r.held, "change")} git would see` : ""}`);
2724
- return `${where}: tidied ${plural(r.skills, "skill")}${r.held ? `; left ${plural(r.held, "change")} git would see` : ""}`;
2725
- };
2726
- issues.push({
2727
- id: `tidy:${root}`,
2728
- severity: "suggestion",
2729
- title: `${where}: ${plural(tidy2.plans.length, "skill")} with extra copies or links`,
2730
- detail: tidy2.plans.map((p) => `${p.name}: ${p.steps.map((s) => describeStep(s, root)).join(", ")}`).join(" \xB7 ") + (visible2.length ? `. Git would see ${plural(visible2.length, "of these change")}: ${visible2.join(", ")}.` : tidy2.plans.some((p) => p.steps.some((s) => s.kind === "replace" || s.kind === "remove")) ? ". Replaced copies are restorable with skilllib restore." : "."),
2731
- at: rows(root, tidy2.plans.map((p) => p.name)),
2732
- ...visible2.length ? {
2733
- choices: [
2734
- // "Keep git as it is" only when it leaves something to do.
2735
- ...visible2.length < tidy2.plans.reduce((n, p) => n + p.steps.length, 0) ? [{ label: "Tidy, but keep git as it is", hint: `skips the ${plural(visible2.length, "change")} git would see`, run: run("keep") }] : [],
2736
- { label: "Tidy everything", hint: `${plural(visible2.length, "change")} will show in git status`, run: run("go") }
2737
- ]
2738
- } : { fix: { label: "Tidy", run: run("go") } }
2739
- });
2740
- }
2741
- issues.push(...tidy2.conflicts.map(conflictIssue));
2742
- for (const s of tidy2.skipped.filter((x) => x.differs))
2743
- issues.push({
2744
- id: `linkdiffers:${root}:${s.name}`,
2745
- severity: "problem",
2746
- title: `${s.name} in ${where}: the copies differ`,
2747
- detail: `${s.reason}, with other content than the copy beside it: your agents run different versions. Make them the same, or remove the link; skilllib doesn't change a link to another copy.`,
2748
- at: rows(root, [s.name])
2749
- });
2750
- const loadsGlobally = (name, id) => {
2751
- const here = status2.find((s) => s.name === name);
2752
- return globalCopies.some((m) => m.name === name && m.harnesses.includes(id) && (!here || treeHash(m.path) === treeHash(here.path)));
2753
- };
2754
- const blindHere = status2.filter((s) => s.state !== "folder missing" && s.visibility.some((v) => v.paths === 0 && !loadsGlobally(s.name, v.id))).map((s) => ({ s, would: linkEverywhere(root, s.name, s.location, { dryRun: true }) }));
2755
- const enabledIds = enabledHarnesses();
2756
- const notFolders = /* @__PURE__ */ new Set();
2757
- for (const dir of new Set(enabledIds.map((id) => harness(id).projectDirs[0]))) {
2758
- const blocker = notWritable(root, dir);
2759
- if (!blocker || notFolders.has(blocker)) continue;
2760
- notFolders.add(blocker);
2761
- const who = agentNames(enabledIds.filter((id) => notWritable(root, harness(id).projectDirs[0]) === blocker));
2762
- const out2 = linksOut(root, blocker);
2763
- issues.push({
2764
- id: `notdir:${root}:${blocker}`,
2765
- severity: out2 ? "suggestion" : "problem",
2766
- title: out2 ? `${where}: ${blocker} links out of the repo` : `${where}: ${blocker} isn't a folder`,
2767
- detail: out2 ? `${blockerSaid(root, blocker)}. ${who} ${who.includes(",") || who.includes(" and ") ? "load" : "loads"} what's there, but skilllib treats it as outside ${where}: it adds, links and removes nothing there.` : `${blockerSaid(root, blocker)}: ${who} can't load skills from ${where}, and skilllib adds none there. Move it out of the way, then run skilllib link.`,
2768
- at: [{ root, skill: "" }]
2769
- // the repo itself: its row in Places
2770
- });
2771
- }
2772
- for (const { s, would } of blindHere) {
2773
- const way = would.inTheWay.filter((p) => !notFolders.has(p));
2774
- if (!way.length || blockers.has(s.name)) continue;
2775
- issues.push({
2776
- id: `intheway:${root}:${s.name}`,
2777
- severity: "problem",
2778
- title: `${s.name} in ${where}: ${agentNames(s.visibility.filter((v) => v.paths === 0).map((v) => v.id))} can't use it here`,
2779
- detail: `${inTheWaySaid(root, way)}. skilllib won't link past what's there: clear the way, then run skilllib link.`,
2780
- at: rows(root, [s.name])
2781
- });
2782
- }
2783
- for (const { s, would } of blindHere.filter(({ would: w }) => !w.created.length && !w.blocked.length && w.uncommitted.length))
2784
- issues.push({
2785
- id: `uncommitted:${root}:${s.name}`,
2786
- severity: "suggestion",
2787
- title: `${s.name} in ${where}: ${agentNames(s.visibility.filter((v) => v.paths === 0).map((v) => v.id))} can't use it until it's committed`,
2788
- detail: `Git tracks ${would.uncommitted.join(", ")}, but not ${s.location}/${s.name}: a link there would be broken for teammates. Commit it, then link it (\`skilllib link --allow-tracked\`).`,
2789
- at: rows(root, [s.name])
2790
- });
2791
- const partial = blindHere.filter(({ would }) => would.created.length || would.blocked.length).map(({ s }) => s);
2792
- if (partial.length) {
2793
- const missing2 = [...new Set(partial.flatMap((s) => s.visibility.filter((v) => v.paths === 0).map((v) => v.id)))];
2794
- const tracked = [...new Set(blindHere.flatMap(({ would }) => would.blocked))];
2795
- const manifestShared = partial.some((s) => s.managed) && isShared(gitOf(root)?.of(MANIFEST_FILE));
2796
- const run = (allowTracked) => () => {
2797
- if (allowTracked) allowTrackedLinks(root);
2798
- const r = linkAll(root, { allowTracked, names: partial.map((s) => s.name) });
2799
- const notes = `${r.blocked.length ? `; skipped ${r.blocked.join(", ")} (git tracks ${r.blocked.length === 1 ? "it" : "them"})` : ""}${r.uncommitted.length ? `; ${r.uncommitted.join(", ")} not linked into folders git tracks (not committed, so teammates would get broken links)` : ""}`;
2800
- if (!r.linked.length) {
2801
- const still = partial.filter((s) => visibilityOf(root, s.name).some((v) => v.paths === 0 && !loadsGlobally(s.name, v.id)));
2802
- if (!still.length) return `${where}: already usable by ${agentNames(missing2)}`;
2803
- throw new Error(`${where}: nothing linked${notes}`);
2804
- }
2805
- return `${where}: linked ${plural(r.linked.length, "skill")} for ${agentNames(missing2)}${notes}`;
2806
- };
2807
- issues.push({
2808
- id: `usable:${root}`,
2809
- severity: "suggestion",
2810
- title: `${where}: ${plural(partial.length, "skill")} not usable by ${agentNames(missing2)}`,
2811
- detail: `${partial.map((s) => `${s.name} (not ${agentNames(s.visibility.filter((v) => v.paths === 0).map((v) => v.id))})`).join(", ")}. Adding links makes the one real copy reach every agent you use; nothing is copied or moved.${tracked.length ? ` Git tracks ${tracked.join(", ")}, so links there show in git status.` : ""}${manifestShared ? " The new links are recorded in skilllib.json, which git tracks." : ""}`,
2812
- at: rows(root, partial.map((s) => s.name)),
2813
- ...manifestShared && !tracked.length ? { choices: [{ label: "Add the links", hint: "git will see skilllib.json change", run: run(false) }] } : tracked.length ? {
2814
- choices: [
2815
- // Only when some link goes where git doesn't track: otherwise this one would link nothing.
2816
- ...blindHere.some(({ would }) => would.created.length) ? [{ label: "Add links, but not where git tracks the folder", hint: `skips ${tracked.join(", ")}`, run: run(false) }] : [],
2817
- { label: "Add links everywhere", hint: `remembered for ${where}; git will see them`, run: run(true) }
2818
- ]
2819
- } : { fix: { label: "Add the links", run: run(false) } }
2820
- });
2821
- }
2822
- const differing = new Set(tidy2.conflicts.map((c) => c.name));
2823
- const git2 = gitOf(root);
2824
- const committed = (s) => isShared(git2?.of(relativeTo(root, s.path)));
2825
- for (const s of unstuck) {
2826
- if (isLink(s.path) || differing.has(s.name)) continue;
2827
- if (s.state === "local only" && (committed(s) || untrackable(s))) {
2828
- const why = committed(s) ? "it's the team's (committed)" : `${untrackable(s)}, so skilllib can't track it here`;
2829
- issues.push({
2830
- id: `local:${root}:${s.name}`,
2831
- severity: "suggestion",
2832
- title: `${s.name} in ${where} isn't in your library`,
2833
- detail: `Copy it into the library to reuse it in other projects. The repo's copy stays as it is: ${why}.`,
2834
- at: rows(root, [s.name]),
2835
- choices: [
2836
- {
2837
- label: "Copy into the library",
2838
- hint: `${where}'s copy stays as it is`,
2839
- run: () => {
2840
- intoLibrary(s.path);
2841
- return `${s.name} copied into your library; ${where}'s copy stays as it is`;
2842
- }
2843
- }
2844
- ]
2845
- });
2846
- } else if (s.state === "local only") {
2847
- issues.push({
2848
- id: `local:${root}:${s.name}`,
2849
- severity: "suggestion",
2850
- title: `${s.name} in ${where} isn't in your library`,
2851
- detail: "Import it into the library to reuse it in other projects and keep it backed up. The repo then tracks it in skilllib.json.",
2852
- at: rows(root, [s.name]),
2853
- choices: [
2854
- {
2855
- label: "Import into the library",
2856
- // Tracking it writes skilllib.json.
2857
- hint: `copies it into Your skills${gitNote(root, [join13(root, MANIFEST_FILE)])}`,
2858
- run: () => {
2859
- intoLibrary(s.path);
2860
- track(root, s.name);
2861
- return `${s.name} imported and tracked in ${where}`;
2862
- }
2863
- }
2864
- ]
2865
- });
2866
- } else if (s.state === "untracked copy of library skill" && !committed(s) && !untrackable(s)) {
2867
- issues.push({
2868
- id: `adopt:${root}:${s.name}`,
2869
- severity: "suggestion",
2870
- title: `${s.name} in ${where} isn't tracked`,
2871
- detail: "It's identical to the library copy; track it (in skilllib.json) so future library changes reach it.",
2872
- at: rows(root, [s.name]),
2873
- choices: [{ label: "Track it", hint: `adds it to ${where}'s skilllib.json`, run: () => `${track(root, s.name).name} tracked in ${where}` }]
2874
- });
2875
- } else if (s.state === "untracked, differs from library" || s.state === "repo skill, differs from library") {
2876
- const teams = s.state.startsWith("repo skill") || committed(s) || !!untrackable(s);
2877
- const whose = s.state.startsWith("repo skill") || committed(s) ? "It's the team's copy (committed, or in the repo's own .agents/skills or .grok/skills)" : `${untrackable(s)}, so skilllib can't track it here`;
2878
- issues.push({
2879
- id: `differs:${root}:${s.name}`,
2880
- severity: "suggestion",
2881
- title: `${s.name} in ${where} differs from your library`,
2882
- detail: teams ? `${whose}, and no version in Your skills matches it. Update your library from it if it's the one to keep; ${where}'s copy stays as it is.` : `No version in Your skills matches it. Update your library from it (${where} then tracks it), or replace it with your library's.`,
2883
- at: rows(root, [s.name]),
2884
- choices: [
2885
- {
2886
- label: "Update your library from it",
2887
- hint: teams ? `a new version in Your skills; ${where}'s copy stays as it is` : `a new version in Your skills; ${where} then tracks it`,
2888
- run: () => {
2889
- intoLibrary(s.path, true);
2890
- if (teams) return `${s.name}: your library has ${where}'s copy; ${where}'s copy stays as it is`;
2891
- const c = track(root, s.name);
2892
- return `${s.name}: your library has ${where}'s copy as v${c.to}; ${where} tracks it`;
2893
- }
2894
- },
2895
- ...teams ? [] : [
2896
- {
2897
- label: "Replace it with your library's",
2898
- hint: "this copy is restorable with skilllib restore",
2899
- run: () => {
2900
- const c = addSkill(root, s.name, { force: true });
2901
- if (c.action === "skipped") throw new Error(`${s.name}: ${c.reason}`);
2902
- return `${s.name} in ${where} is your library's v${c.to} now; the old copy is restorable with skilllib restore`;
2903
- }
2904
- }
2905
- ]
2906
- ]
2907
- });
2908
- } else if (s.state === "from npx skills" && !((committed(s) || untrackable(s)) && versionForHash(s.name, treeHash(s.path) ?? "") !== null)) {
2909
- const teams = committed(s) || !!untrackable(s);
2910
- issues.push({
2911
- id: `npx:${root}:${s.name}`,
2912
- severity: "suggestion",
2913
- title: `${s.name} in ${where} came from npx skills, not your library`,
2914
- detail: `Installed with \`npx skills add\`${s.source ? ` (${s.source})` : ""}. Import it into your library to reuse it in other projects and keep it backed up${teams ? `; ${where}'s copy stays as it is (${committed(s) ? "committed" : `${untrackable(s)}, so skilllib can't track it here`})` : `; ${where} then tracks it in skilllib.json`}.`,
2915
- at: rows(root, [s.name]),
2916
- choices: [
2917
- {
2918
- label: "Import it into your library",
2919
- hint: teams ? `${where}'s copy stays as it is` : `${where} then tracks it`,
2920
- run: () => {
2921
- intoLibrary(s.path);
2922
- if (teams) return `${s.name} copied into your library; ${where}'s copy stays as it is`;
2923
- track(root, s.name);
2924
- return `${s.name} imported and tracked in ${where}`;
2925
- }
2926
- }
2927
- ]
2928
- });
2929
- }
2930
- if (s.state === "not in library")
2931
- issues.push({
2932
- id: `not-in-lib:${root}:${s.name}`,
2933
- severity: "suggestion",
2934
- title: `${s.name} in ${where} is tracked, but no longer in your library`,
2935
- detail: `${where}'s skilllib.json pins it, but your library has no copy: if the folder goes, sync can't restore it, and your other repos can't add it. Copy ${where}'s copy back into the library.`,
2936
- at: rows(root, [s.name]),
2937
- choices: [
2938
- {
2939
- label: "Copy it back into your library",
2940
- hint: `${where}'s copy stays as it is`,
2941
- run: () => {
2942
- intoLibrary(s.path);
2943
- return `${s.name} copied back into your library`;
2944
- }
2945
- }
2946
- ]
2947
- });
2948
- if (s.state === "edited locally" || s.state === "edited locally, update available") {
2949
- const newer2 = s.state !== "edited locally";
2950
- const pinned = s.version ? `v${s.version}` : "the library version";
2951
- issues.push({
2952
- id: `edited:${root}:${s.name}`,
2953
- severity: "suggestion",
2954
- title: `${s.name} in ${where} has local edits`,
2955
- detail: `It no longer matches ${pinned} from Your skills${newer2 ? `, and v${s.latest} is out` : ""}. ${untrackable(s) ? `${untrackable(s)}, so skilllib can't re-pin or discard them here; copy them into Your skills to keep them for your other repos.` : "Save the edits as a new version so your other repos can get them, or discard them. Keeping them is fine too: skilllib never overwrites edits."}`,
2956
- at: rows(root, [s.name]),
2957
- choices: untrackable(s) ? [
2958
- {
2959
- label: "Copy the edits into Your skills",
2960
- hint: `${newer2 ? `becomes v${s.latest + 1}; v${s.latest}'s changes aren't merged in` : "a new version there"}; ${where}'s copy and pin stay as they are (skilllib sync there would put the pinned version back)`,
2961
- run: () => {
2962
- intoLibrary(s.path, true);
2963
- return `${s.name}: the edits are in Your skills as v${latestVersion(s.name)?.version}; ${where} stays as it is`;
2964
- }
2965
- }
2966
- ] : [
2967
- {
2968
- label: "Save as a new version in Your skills",
2969
- hint: `${newer2 ? `becomes v${s.latest + 1}; v${s.latest}'s changes aren't merged in` : `becomes v${(s.latest ?? 0) + 1}; ${where} uses it`}${gitNote(root, [join13(root, MANIFEST_FILE)])}`,
2970
- run: () => {
2971
- importSkill(s.path, { force: true });
2972
- const c = addSkill(root, s.name);
2973
- if (c.action === "skipped") throw new Error(`${s.name}: ${c.reason}`);
2974
- return `${s.name}: saved as v${c.to}; ${where} uses it`;
2975
- }
2976
- },
2977
- {
2978
- label: "Discard the edits",
2979
- hint: `back to ${pinned}; the edited copy is restorable with skilllib restore${gitNote(root, [s.path, join13(root, MANIFEST_FILE)])}`,
2980
- run: () => {
2981
- const c = discardEdits(root, s.name);
2982
- if (c.action === "skipped") throw new Error(`${s.name}: ${c.reason}`);
2983
- return `${s.name}: edits discarded, back to v${c.to}`;
2984
- }
2985
- }
2986
- ]
2987
- });
2988
- }
2989
- if (alsoLoadedGlobally(s)) alsoGlobal.set(s.name, [...alsoGlobal.get(s.name) ?? [], { root, managed: s.managed }]);
2990
- }
2991
- }
2777
+ const ctx = { issues, statuses, openedProjects, globalCopies, alsoLoadedGlobally, alsoGlobal };
2778
+ for (const root of projects2) repoIssues(root, ctx);
2992
2779
  for (const root of projects2) {
2993
2780
  for (const n of nested.get(root)) {
2994
2781
  const copies = alsoGlobal.get(n.name) ?? [];
@@ -3001,23 +2788,23 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
3001
2788
  const subsOf = (root) => subsMemo.get(root) ?? (subsMemo.set(root, packageDirs(root)), subsMemo.get(root));
3002
2789
  const entriesOf2 = (root) => nestedMemo.get(root) ?? (nestedMemo.set(root, nestedEntries(root, { subs: subsOf(root) })), nestedMemo.get(root));
3003
2790
  for (const [name, copies] of alsoGlobal) {
3004
- const wheres = copies.map((c) => basename6(c.root)).join(", ");
2791
+ const wheres = copies.map((c) => basename7(c.root)).join(", ");
3005
2792
  const removable = copies.filter((c) => c.managed && statuses.get(c.root)?.some((s) => s.name === name && removesClean(c.root, s))).map((c) => c.root);
3006
- const repoOwn = copies.filter((c) => !c.managed).map((c) => basename6(c.root));
2793
+ const repoOwn = copies.filter((c) => !c.managed).map((c) => basename7(c.root));
3007
2794
  const at = [...copies.flatMap((c) => rows(c.root, [name])), ...rows(null, [name])];
3008
2795
  if (kept.has(name)) {
3009
2796
  const globalLoaders = new Set(globalCopies.filter((m) => m.name === name).flatMap((m) => m.harnesses));
3010
2797
  const onlyHere = (root) => visibilityOf(root, name).filter((v) => v.paths > 0 && !globalLoaders.has(v.id)).map((v) => harness(v.id).name);
3011
2798
  const needed = removable.filter((root) => onlyHere(root).length);
3012
2799
  const extra = removable.filter((root) => !onlyHere(root).length);
3013
- const removeWheres = extra.map((r) => basename6(r)).join(", ");
2800
+ const removeWheres = extra.map((r) => basename7(r)).join(", ");
3014
2801
  issues.push({
3015
2802
  id: `twice:${name}`,
3016
2803
  severity: "suggestion",
3017
2804
  title: `${name}: in ${wheres}, and you keep it global`,
3018
2805
  detail: [
3019
2806
  extra.length ? `The global copy already loads in ${removeWheres} for every agent that reads the project copy, so those are extra.` : "",
3020
- needed.length ? `In ${needed.map((r) => `${basename6(r)} (${onlyHere(r).join(", ")})`).join(", ")}, some agents read only the repo's copy: keep it there, or link the global one for them.` : "",
2807
+ needed.length ? `In ${needed.map((r) => `${basename7(r)} (${onlyHere(r).join(", ")})`).join(", ")}, some agents read only the repo's copy: keep it there, or link the global one for them.` : "",
3021
2808
  repoOwn.length ? `${repoOwn.join(", ")} ${repoOwn.length === 1 ? "has its own copy" : "have their own copies"}: remove ${repoOwn.length === 1 ? "it" : "them"} in the repo, or stop keeping ${name} global.` : ""
3022
2809
  ].filter(Boolean).join(" "),
3023
2810
  at,
@@ -3028,8 +2815,8 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
3028
2815
  hint: `the global copy stays${extra.some((r) => gitNote(r, [...statuses.get(r).filter((s) => s.name === name).map((s) => s.path), join13(r, MANIFEST_FILE)])) ? "; git will see it" : ""}`,
3029
2816
  run: () => {
3030
2817
  const results = extra.map((root) => [root, removeSkill(root, name)]);
3031
- const removed = results.filter(([, c]) => c.action === "removed").map(([r]) => basename6(r));
3032
- const kept2 = results.filter(([, c]) => c.action !== "removed").map(([r, c]) => `${basename6(r)} (${c.reason})`);
2818
+ const removed = results.filter(([, c]) => c.action === "removed").map(([r]) => basename7(r));
2819
+ const kept2 = results.filter(([, c]) => c.action !== "removed").map(([r, c]) => `${basename7(r)} (${c.reason})`);
3033
2820
  if (kept2.length) throw new Error(`${name}: ${removed.length ? `removed from ${removed.join(", ")}; ` : ""}kept in ${kept2.join(", ")}`);
3034
2821
  return `${name}: removed from ${removed.join(", ")}; the global copy stays`;
3035
2822
  }
@@ -3090,6 +2877,404 @@ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, load
3090
2877
  }
3091
2878
  return issues;
3092
2879
  }
2880
+ function repoIssues(root, ctx) {
2881
+ const where = basename7(root);
2882
+ const status2 = ctx.statuses.get(root);
2883
+ ctx.issues.push(...brokenLinksIn(root, ctx.openedProjects));
2884
+ const pins = readManifest(root).skills;
2885
+ const shadowed = Object.keys(personalPins(root)).filter((name) => pins[name] && !pins[name].personal);
2886
+ if (shadowed.length)
2887
+ ctx.issues.push({
2888
+ id: `shadowed:${root}`,
2889
+ severity: "suggestion",
2890
+ title: `${where}: ${plural(shadowed.length, "skill")} pinned in both skilllib.json and your personal pins`,
2891
+ detail: `${shadowed.join(", ")}: ${where}'s skilllib.json pins ${shadowed.length === 1 ? "it" : "them"} for the team, and that pin wins. Your personal pin does nothing.`,
2892
+ at: rows(root, shadowed),
2893
+ choices: [
2894
+ {
2895
+ label: "Drop the personal pins",
2896
+ hint: "skilllib.json's pins stay; nothing that loads changes",
2897
+ run: () => {
2898
+ for (const name of shadowed) dropPersonalPin(root, name);
2899
+ return `${where}: ${shadowed.join(", ")} now pinned only in skilllib.json`;
2900
+ }
2901
+ }
2902
+ ]
2903
+ });
2904
+ const blockedFor = /* @__PURE__ */ new Map();
2905
+ const untrackable = (s) => {
2906
+ if (!blockedFor.has(s.name)) blockedFor.set(s.name, trackBlocker(root, s.name));
2907
+ return blockedFor.get(s.name);
2908
+ };
2909
+ const blockers = new Map(status2.filter((s) => s.managed).flatMap((s) => ((b) => b ? [[s.name, b]] : [])(syncBlocker(root, s.name, pins[s.name]))));
2910
+ for (const [name, blocker] of blockers)
2911
+ ctx.issues.push({
2912
+ id: `stuck:${root}:${name}`,
2913
+ severity: "problem",
2914
+ title: `${name} in ${where}: sync can't put it back as skilllib.json pins it`,
2915
+ // What to do depends on what's there: a folder that links out of the repo is a setup of yours, and a pin
2916
+ // your library has no version of can't come back by sync anyway.
2917
+ detail: `${blocker.said}, so sync can't put it back there.${blocker.outside ? " skilllib only installs inside the repo: point that folder back into the repo, or remove the skill here (skilllib remove)." : versionForHash(name, pins[name]?.hash ?? "") !== null ? " Move it out of the way, then run skilllib sync." : " Move it out of the way; your library has no copy of the pinned version either."}`,
2918
+ at: rows(root, [name])
2919
+ });
2920
+ const unstuck = status2.filter((s) => !blockers.has(s.name));
2921
+ const missing = unstuck.filter((s) => s.state === "folder missing" && s.latest !== null);
2922
+ const knownPin = (s) => versionForHash(s.name, pins[s.name]?.hash ?? "") !== null;
2923
+ const restorable = missing.filter(knownPin);
2924
+ for (const s of unstuck.filter((m) => (m.state === "folder missing" || m.state === "out of sync") && m.latest !== null && !knownPin(m)))
2925
+ ctx.issues.push({
2926
+ id: `pin:${root}:${s.name}`,
2927
+ severity: "problem",
2928
+ title: `${s.name}: the version ${where}'s skilllib.json pins isn't in your library`,
2929
+ detail: `${s.state === "folder missing" ? "The folder is missing, and no" : "The copy is another version, and no"} version in Your skills has the content it pins (pinned from another library?). ${untrackable(s) ? `${untrackable(s)}, so skilllib can't install it here.` : `Install your newest v${s.latest}; ${where}'s skilllib.json then pins it.`}`,
2930
+ at: rows(root, [s.name]),
2931
+ // It changes what the shared skilllib.json pins: a choice, never run by `doctor --fix`.
2932
+ ...untrackable(s) ? {} : { choices: [{ label: `Install your newest (v${s.latest})`, hint: `${where}'s skilllib.json pins it`, run: () => `${s.name}: installed v${track(root, s.name).to}` }] }
2933
+ });
2934
+ if (restorable.length > 0) {
2935
+ ctx.issues.push({
2936
+ id: `sync:${root}`,
2937
+ severity: "problem",
2938
+ title: `${where}: ${plural(restorable.length, "skill")} pinned but not installed`,
2939
+ detail: `${restorable.map((s) => s.name).join(", ")}: the folder is missing`,
2940
+ at: rows(root, restorable.map((s) => s.name)),
2941
+ fix: {
2942
+ label: "Sync the project",
2943
+ // Only the missing folders: an out-of-sync copy (maybe committed) is its own issue's choice.
2944
+ run: () => summarize(where, "restored", syncProject(root, { names: restorable.map((s) => s.name) }))
2945
+ }
2946
+ });
2947
+ }
2948
+ const drifted = unstuck.filter((s) => s.state === "out of sync" && knownPin(s));
2949
+ if (drifted.length) {
2950
+ const seen = drifted.some((s) => isShared(gitOf(root)?.of(relativeTo(root, s.path))));
2951
+ const yours = drifted.some((s) => untrackable(s));
2952
+ const install = {
2953
+ label: "Install the pinned versions",
2954
+ hint: seen ? "git will see the copies change" : "your library keeps every version",
2955
+ run: () => summarize(where, "synced", syncProject(root, { names: drifted.map((s) => s.name) }))
2956
+ };
2957
+ ctx.issues.push({
2958
+ id: `out-of-sync:${root}`,
2959
+ severity: "suggestion",
2960
+ title: `${where}: ${plural(drifted.length, "skill")} not at the pinned version`,
2961
+ detail: `${drifted.map((s) => s.name).join(", ")}: each copy is another version from your library. sync installs the pinned one.${yours ? ` ${[...new Set(drifted.flatMap((s) => untrackable(s) ? [untrackable(s)] : []))].map((why) => `${drifted.filter((s) => untrackable(s) === why).map((s) => s.name).join(", ")}: ${why}`).join("; ")}, so skilllib can't pin the version that's there; ${drifted.filter((s) => untrackable(s)).length === 1 ? "it may be edits you copied into Your skills on purpose, and stays" : "they may be edits you copied into Your skills on purpose, and stay"} unless you choose this.` : ""}`,
2962
+ at: rows(root, drifted.map((s) => s.name)),
2963
+ ...seen || yours ? { choices: [install] } : { fix: install }
2964
+ });
2965
+ }
2966
+ for (const s of unstuck.filter((s2) => s2.state === "folder missing" && s2.latest === null))
2967
+ ctx.issues.push({
2968
+ id: `unavailable:${root}:${s.name}`,
2969
+ severity: "problem",
2970
+ title: `${s.name}: pinned in ${where}'s skilllib.json, but not in your library`,
2971
+ detail: `Its folder is missing, and your library (${tildify(libraryDir())}) has no copy to restore: import it, or set SKILLLIB_HOME to the skilllib folder whose library has it.`,
2972
+ at: rows(root, [s.name]),
2973
+ // Editing skilllib.json is the team's call: a choice, never run by `doctor --fix`.
2974
+ choices: [
2975
+ {
2976
+ label: "Remove it from skilllib.json",
2977
+ hint: `${where} stops pinning it`,
2978
+ run: () => {
2979
+ const r = removeSkill(root, s.name);
2980
+ if (r.action !== "removed") throw new Error(`${s.name}: ${r.reason}`);
2981
+ return `${s.name}: removed from ${where}'s skilllib.json`;
2982
+ }
2983
+ }
2984
+ ]
2985
+ });
2986
+ const behind = unstuck.filter((s) => s.state === "update available" && !untrackable(s));
2987
+ if (behind.length > 0)
2988
+ ctx.issues.push({
2989
+ id: `update:${root}`,
2990
+ severity: "suggestion",
2991
+ title: `${where}: ${plural(behind.length, "skill")} out of date`,
2992
+ detail: behind.map((s) => `${s.name} (v${s.version} \u2192 v${s.latest})`).join(", "),
2993
+ at: rows(root, behind.map((s) => s.name)),
2994
+ // It changes what the repo pins and the copies its agents load (often committed): a choice, never `doctor --fix`.
2995
+ choices: [
2996
+ {
2997
+ label: "Update to the newest versions",
2998
+ hint: isShared(gitOf(root)?.of(MANIFEST_FILE)) ? "git will see the new copies and skilllib.json" : "replaces each copy with your library's newest",
2999
+ run: () => summarize(where, "updated", updateProject(root, behind.map((s) => s.name)))
3000
+ }
3001
+ ]
3002
+ });
3003
+ const tidy2 = planProjectTidy(root);
3004
+ if (tidy2.plans.length) {
3005
+ const git3 = gitOf(root);
3006
+ const visible2 = tidy2.plans.flatMap((p) => gitVisibleSteps(p, git3).map((s) => describeStep(s, root)));
3007
+ const run = (mode) => () => {
3008
+ const r = applyAll(tidy2.plans, mode);
3009
+ if (!r.skills) throw new Error(`${where}: nothing tidied${r.held ? `; left ${plural(r.held, "change")} git would see` : ""}`);
3010
+ return `${where}: tidied ${plural(r.skills, "skill")}${r.held ? `; left ${plural(r.held, "change")} git would see` : ""}`;
3011
+ };
3012
+ ctx.issues.push({
3013
+ id: `tidy:${root}`,
3014
+ severity: "suggestion",
3015
+ title: `${where}: ${plural(tidy2.plans.length, "skill")} with extra copies or links`,
3016
+ detail: tidy2.plans.map((p) => `${p.name}: ${p.steps.map((s) => describeStep(s, root)).join(", ")}`).join(" \xB7 ") + (visible2.length ? `. Git would see ${plural(visible2.length, "of these change")}: ${visible2.join(", ")}.` : tidy2.plans.some((p) => p.steps.some((s) => s.kind === "replace" || s.kind === "remove")) ? ". Replaced copies are restorable with skilllib restore." : "."),
3017
+ at: rows(root, tidy2.plans.map((p) => p.name)),
3018
+ ...visible2.length ? {
3019
+ choices: [
3020
+ // "Keep git as it is" only when it leaves something to do.
3021
+ ...visible2.length < tidy2.plans.reduce((n, p) => n + p.steps.length, 0) ? [{ label: "Tidy, but keep git as it is", hint: `skips the ${plural(visible2.length, "change")} git would see`, run: run("keep") }] : [],
3022
+ { label: "Tidy everything", hint: `${plural(visible2.length, "change")} will show in git status`, run: run("go") }
3023
+ ]
3024
+ } : { fix: { label: "Tidy", run: run("go") } }
3025
+ });
3026
+ }
3027
+ ctx.issues.push(...tidy2.conflicts.map(conflictIssue));
3028
+ for (const s of tidy2.skipped.filter((x) => x.differs))
3029
+ ctx.issues.push({
3030
+ id: `linkdiffers:${root}:${s.name}`,
3031
+ severity: "problem",
3032
+ title: `${s.name} in ${where}: the copies differ`,
3033
+ detail: `${s.reason}, with other content than the copy beside it: your agents run different versions. Make them the same, or remove the link; skilllib doesn't change a link to another copy.`,
3034
+ at: rows(root, [s.name])
3035
+ });
3036
+ const loadsGlobally = (name, id) => {
3037
+ const here = status2.find((s) => s.name === name);
3038
+ return ctx.globalCopies.some((m) => m.name === name && m.harnesses.includes(id) && (!here || treeHash(m.path) === treeHash(here.path)));
3039
+ };
3040
+ const blindHere = status2.filter((s) => s.state !== "folder missing" && s.visibility.some((v) => v.paths === 0 && !loadsGlobally(s.name, v.id))).map((s) => ({ s, would: linkEverywhere(root, s.name, s.location, { dryRun: true }) }));
3041
+ const enabledIds = enabledHarnesses();
3042
+ const notFolders = /* @__PURE__ */ new Set();
3043
+ for (const dir of new Set(enabledIds.map((id) => harness(id).projectDirs[0]))) {
3044
+ const blocker = notWritable(root, dir);
3045
+ if (!blocker || notFolders.has(blocker)) continue;
3046
+ notFolders.add(blocker);
3047
+ const who = agentNames(enabledIds.filter((id) => notWritable(root, harness(id).projectDirs[0]) === blocker));
3048
+ const out2 = linksOut(root, blocker);
3049
+ ctx.issues.push({
3050
+ id: `notdir:${root}:${blocker}`,
3051
+ severity: out2 ? "suggestion" : "problem",
3052
+ title: out2 ? `${where}: ${blocker} links out of the repo` : `${where}: ${blocker} isn't a folder`,
3053
+ detail: out2 ? `${blockerSaid(root, blocker)}. ${who} ${who.includes(",") || who.includes(" and ") ? "load" : "loads"} what's there, but skilllib treats it as outside ${where}: it adds, links and removes nothing there.` : `${blockerSaid(root, blocker)}: ${who} can't load skills from ${where}, and skilllib adds none there. Move it out of the way, then run skilllib link.`,
3054
+ at: [{ root, skill: "" }]
3055
+ // the repo itself: its row in Places
3056
+ });
3057
+ }
3058
+ for (const { s, would } of blindHere) {
3059
+ const way = would.inTheWay.filter((p) => !notFolders.has(p));
3060
+ if (!way.length || blockers.has(s.name)) continue;
3061
+ ctx.issues.push({
3062
+ id: `intheway:${root}:${s.name}`,
3063
+ severity: "problem",
3064
+ title: `${s.name} in ${where}: ${agentNames(s.visibility.filter((v) => v.paths === 0).map((v) => v.id))} can't use it here`,
3065
+ detail: `${inTheWaySaid(root, way)}. skilllib won't link past what's there: clear the way, then run skilllib link.`,
3066
+ at: rows(root, [s.name])
3067
+ });
3068
+ }
3069
+ for (const { s, would } of blindHere.filter(({ would: w }) => !w.created.length && !w.blocked.length && w.uncommitted.length))
3070
+ ctx.issues.push({
3071
+ id: `uncommitted:${root}:${s.name}`,
3072
+ severity: "suggestion",
3073
+ title: `${s.name} in ${where}: ${agentNames(s.visibility.filter((v) => v.paths === 0).map((v) => v.id))} can't use it until it's committed`,
3074
+ detail: `Git tracks ${would.uncommitted.join(", ")}, but not ${s.location}/${s.name}: a link there would be broken for teammates. Commit it, then link it (\`skilllib link --allow-tracked\`).`,
3075
+ at: rows(root, [s.name])
3076
+ });
3077
+ const partial = blindHere.filter(({ would }) => would.created.length || would.blocked.length).map(({ s }) => s);
3078
+ if (partial.length) {
3079
+ const missing2 = [...new Set(partial.flatMap((s) => s.visibility.filter((v) => v.paths === 0).map((v) => v.id)))];
3080
+ const tracked = [...new Set(blindHere.flatMap(({ would }) => would.blocked))];
3081
+ const manifestShared = partial.some((s) => s.managed) && isShared(gitOf(root)?.of(MANIFEST_FILE));
3082
+ const run = (allowTracked) => () => {
3083
+ if (allowTracked) allowTrackedLinks(root);
3084
+ const r = linkAll(root, { allowTracked, names: partial.map((s) => s.name) });
3085
+ const notes = `${r.blocked.length ? `; skipped ${r.blocked.join(", ")} (git tracks ${r.blocked.length === 1 ? "it" : "them"})` : ""}${r.uncommitted.length ? `; ${r.uncommitted.join(", ")} not linked into folders git tracks (not committed, so teammates would get broken links)` : ""}`;
3086
+ if (!r.linked.length) {
3087
+ const still = partial.filter((s) => visibilityOf(root, s.name).some((v) => v.paths === 0 && !loadsGlobally(s.name, v.id)));
3088
+ if (!still.length) return `${where}: already usable by ${agentNames(missing2)}`;
3089
+ throw new Error(`${where}: nothing linked${notes}`);
3090
+ }
3091
+ return `${where}: linked ${plural(r.linked.length, "skill")} for ${agentNames(missing2)}${notes}`;
3092
+ };
3093
+ ctx.issues.push({
3094
+ id: `usable:${root}`,
3095
+ severity: "suggestion",
3096
+ title: `${where}: ${plural(partial.length, "skill")} not usable by ${agentNames(missing2)}`,
3097
+ detail: `${partial.map((s) => `${s.name} (not ${agentNames(s.visibility.filter((v) => v.paths === 0).map((v) => v.id))})`).join(", ")}. Adding links makes the one real copy reach every agent you use; nothing is copied or moved.${tracked.length ? ` Git tracks ${tracked.join(", ")}, so links there show in git status.` : ""}${manifestShared ? " The new links are recorded in skilllib.json, which git tracks." : ""}`,
3098
+ at: rows(root, partial.map((s) => s.name)),
3099
+ ...manifestShared && !tracked.length ? { choices: [{ label: "Add the links", hint: "git will see skilllib.json change", run: run(false) }] } : tracked.length ? {
3100
+ choices: [
3101
+ // Only when some link goes where git doesn't track: otherwise this one would link nothing.
3102
+ ...blindHere.some(({ would }) => would.created.length) ? [{ label: "Add links, but not where git tracks the folder", hint: `skips ${tracked.join(", ")}`, run: run(false) }] : [],
3103
+ { label: "Add links everywhere", hint: `remembered for ${where}; git will see them`, run: run(true) }
3104
+ ]
3105
+ } : { fix: { label: "Add the links", run: run(false) } }
3106
+ });
3107
+ }
3108
+ const differing = new Set(tidy2.conflicts.map((c) => c.name));
3109
+ const git2 = gitOf(root);
3110
+ const committed = (s) => isShared(git2?.of(relativeTo(root, s.path)));
3111
+ for (const s of unstuck) {
3112
+ if (isLink(s.path) || differing.has(s.name)) continue;
3113
+ if (s.state === "local only" && (committed(s) || untrackable(s))) {
3114
+ const why = committed(s) ? "it's the team's (committed)" : `${untrackable(s)}, so skilllib can't track it here`;
3115
+ ctx.issues.push({
3116
+ id: `local:${root}:${s.name}`,
3117
+ severity: "suggestion",
3118
+ title: `${s.name} in ${where} isn't in your library`,
3119
+ detail: `Copy it into the library to reuse it in other projects. The repo's copy stays as it is: ${why}.`,
3120
+ at: rows(root, [s.name]),
3121
+ choices: [
3122
+ {
3123
+ label: "Copy into the library",
3124
+ hint: `${where}'s copy stays as it is`,
3125
+ run: () => {
3126
+ intoLibrary(s.path);
3127
+ return `${s.name} copied into your library; ${where}'s copy stays as it is`;
3128
+ }
3129
+ }
3130
+ ]
3131
+ });
3132
+ } else if (s.state === "local only") {
3133
+ ctx.issues.push({
3134
+ id: `local:${root}:${s.name}`,
3135
+ severity: "suggestion",
3136
+ title: `${s.name} in ${where} isn't in your library`,
3137
+ detail: "Import it into the library to reuse it in other projects and keep it backed up. The repo then tracks it in skilllib.json.",
3138
+ at: rows(root, [s.name]),
3139
+ choices: [
3140
+ {
3141
+ label: "Import into the library",
3142
+ // Tracking it writes skilllib.json.
3143
+ hint: `copies it into Your skills${gitNote(root, [join13(root, MANIFEST_FILE)])}`,
3144
+ run: () => {
3145
+ intoLibrary(s.path);
3146
+ track(root, s.name);
3147
+ return `${s.name} imported and tracked in ${where}`;
3148
+ }
3149
+ }
3150
+ ]
3151
+ });
3152
+ } else if (s.state === "untracked copy of library skill" && !committed(s) && !untrackable(s)) {
3153
+ ctx.issues.push({
3154
+ id: `adopt:${root}:${s.name}`,
3155
+ severity: "suggestion",
3156
+ title: `${s.name} in ${where} isn't tracked`,
3157
+ detail: "It's identical to the library copy; track it (in skilllib.json) so future library changes reach it.",
3158
+ at: rows(root, [s.name]),
3159
+ choices: [{ label: "Track it", hint: `adds it to ${where}'s skilllib.json`, run: () => `${track(root, s.name).name} tracked in ${where}` }]
3160
+ });
3161
+ } else if (s.state === "untracked, differs from library" || s.state === "repo skill, differs from library") {
3162
+ const teams = s.state.startsWith("repo skill") || committed(s) || !!untrackable(s);
3163
+ const whose = s.state.startsWith("repo skill") || committed(s) ? "It's the team's copy (committed, or in the repo's own .agents/skills or .grok/skills)" : `${untrackable(s)}, so skilllib can't track it here`;
3164
+ ctx.issues.push({
3165
+ id: `differs:${root}:${s.name}`,
3166
+ severity: "suggestion",
3167
+ title: `${s.name} in ${where} differs from your library`,
3168
+ detail: teams ? `${whose}, and no version in Your skills matches it. Update your library from it if it's the one to keep; ${where}'s copy stays as it is.` : `No version in Your skills matches it. Update your library from it (${where} then tracks it), or replace it with your library's.`,
3169
+ at: rows(root, [s.name]),
3170
+ choices: [
3171
+ {
3172
+ label: "Update your library from it",
3173
+ hint: teams ? `a new version in Your skills; ${where}'s copy stays as it is` : `a new version in Your skills; ${where} then tracks it`,
3174
+ run: () => {
3175
+ intoLibrary(s.path, true);
3176
+ if (teams) return `${s.name}: your library has ${where}'s copy; ${where}'s copy stays as it is`;
3177
+ const c = track(root, s.name);
3178
+ return `${s.name}: your library has ${where}'s copy as v${c.to}; ${where} tracks it`;
3179
+ }
3180
+ },
3181
+ ...teams ? [] : [
3182
+ {
3183
+ label: "Replace it with your library's",
3184
+ hint: "this copy is restorable with skilllib restore",
3185
+ run: () => {
3186
+ const c = addSkill(root, s.name, { force: true });
3187
+ if (c.action === "skipped") throw new Error(`${s.name}: ${c.reason}`);
3188
+ return `${s.name} in ${where} is your library's v${c.to} now; the old copy is restorable with skilllib restore`;
3189
+ }
3190
+ }
3191
+ ]
3192
+ ]
3193
+ });
3194
+ } else if (s.state === "from npx skills" && !((committed(s) || untrackable(s)) && versionForHash(s.name, treeHash(s.path) ?? "") !== null)) {
3195
+ const teams = committed(s) || !!untrackable(s);
3196
+ ctx.issues.push({
3197
+ id: `npx:${root}:${s.name}`,
3198
+ severity: "suggestion",
3199
+ title: `${s.name} in ${where} came from npx skills, not your library`,
3200
+ detail: `Installed with \`npx skills add\`${s.source ? ` (${s.source})` : ""}. Import it into your library to reuse it in other projects and keep it backed up${teams ? `; ${where}'s copy stays as it is (${committed(s) ? "committed" : `${untrackable(s)}, so skilllib can't track it here`})` : `; ${where} then tracks it in skilllib.json`}.`,
3201
+ at: rows(root, [s.name]),
3202
+ choices: [
3203
+ {
3204
+ label: "Import it into your library",
3205
+ hint: teams ? `${where}'s copy stays as it is` : `${where} then tracks it`,
3206
+ run: () => {
3207
+ intoLibrary(s.path);
3208
+ if (teams) return `${s.name} copied into your library; ${where}'s copy stays as it is`;
3209
+ track(root, s.name);
3210
+ return `${s.name} imported and tracked in ${where}`;
3211
+ }
3212
+ }
3213
+ ]
3214
+ });
3215
+ }
3216
+ if (s.state === "not in library")
3217
+ ctx.issues.push({
3218
+ id: `not-in-lib:${root}:${s.name}`,
3219
+ severity: "suggestion",
3220
+ title: `${s.name} in ${where} is tracked, but no longer in your library`,
3221
+ detail: `${where}'s skilllib.json pins it, but your library has no copy: if the folder goes, sync can't restore it, and your other repos can't add it. Copy ${where}'s copy back into the library.`,
3222
+ at: rows(root, [s.name]),
3223
+ choices: [
3224
+ {
3225
+ label: "Copy it back into your library",
3226
+ hint: `${where}'s copy stays as it is`,
3227
+ run: () => {
3228
+ intoLibrary(s.path);
3229
+ return `${s.name} copied back into your library`;
3230
+ }
3231
+ }
3232
+ ]
3233
+ });
3234
+ if (s.state === "edited locally" || s.state === "edited locally, update available") {
3235
+ const newer2 = s.state !== "edited locally";
3236
+ const pinned = s.version ? `v${s.version}` : "the library version";
3237
+ ctx.issues.push({
3238
+ id: `edited:${root}:${s.name}`,
3239
+ severity: "suggestion",
3240
+ title: `${s.name} in ${where} has local edits`,
3241
+ detail: `It no longer matches ${pinned} from Your skills${newer2 ? `, and v${s.latest} is out` : ""}. ${untrackable(s) ? `${untrackable(s)}, so skilllib can't re-pin or discard them here; copy them into Your skills to keep them for your other repos.` : "Save the edits as a new version so your other repos can get them, or discard them. Keeping them is fine too: skilllib never overwrites edits."}`,
3242
+ at: rows(root, [s.name]),
3243
+ choices: untrackable(s) ? [
3244
+ {
3245
+ label: "Copy the edits into Your skills",
3246
+ hint: `${newer2 ? `becomes v${s.latest + 1}; v${s.latest}'s changes aren't merged in` : "a new version there"}; ${where}'s copy and pin stay as they are (skilllib sync there would put the pinned version back)`,
3247
+ run: () => {
3248
+ intoLibrary(s.path, true);
3249
+ return `${s.name}: the edits are in Your skills as v${latestVersion(s.name)?.version}; ${where} stays as it is`;
3250
+ }
3251
+ }
3252
+ ] : [
3253
+ {
3254
+ label: "Save as a new version in Your skills",
3255
+ hint: `${newer2 ? `becomes v${s.latest + 1}; v${s.latest}'s changes aren't merged in` : `becomes v${(s.latest ?? 0) + 1}; ${where} uses it`}${gitNote(root, [join13(root, MANIFEST_FILE)])}`,
3256
+ run: () => {
3257
+ importSkill(s.path, { force: true });
3258
+ const c = addSkill(root, s.name);
3259
+ if (c.action === "skipped") throw new Error(`${s.name}: ${c.reason}`);
3260
+ return `${s.name}: saved as v${c.to}; ${where} uses it`;
3261
+ }
3262
+ },
3263
+ {
3264
+ label: "Discard the edits",
3265
+ hint: `back to ${pinned}; the edited copy is restorable with skilllib restore${gitNote(root, [s.path, join13(root, MANIFEST_FILE)])}`,
3266
+ run: () => {
3267
+ const c = discardEdits(root, s.name);
3268
+ if (c.action === "skipped") throw new Error(`${s.name}: ${c.reason}`);
3269
+ return `${s.name}: edits discarded, back to v${c.to}`;
3270
+ }
3271
+ }
3272
+ ]
3273
+ });
3274
+ }
3275
+ if (ctx.alsoLoadedGlobally(s)) ctx.alsoGlobal.set(s.name, [...ctx.alsoGlobal.get(s.name) ?? [], { root, managed: s.managed }]);
3276
+ }
3277
+ }
3093
3278
  function addedAt(path) {
3094
3279
  try {
3095
3280
  const st = lstatSync4(path);
@@ -3104,7 +3289,7 @@ function usageIssues(projects2, machine, libraryNames, uses, statusOf = projectS
3104
3289
  const issues = [];
3105
3290
  const since = Date.now() - uses.days * 24 * 60 * 60 * 1e3;
3106
3291
  for (const root of projects2) {
3107
- const where = basename6(root);
3292
+ const where = basename7(root);
3108
3293
  const unused = statusOf(root).filter((s) => s.state !== "folder missing" && uses.inProject(root, s.name) === 0 && addedAt(s.path) <= since);
3109
3294
  if (!unused.length) continue;
3110
3295
  const removable = unused.filter((s) => removesClean(root, s));
@@ -3132,15 +3317,15 @@ function usageIssues(projects2, machine, libraryNames, uses, statusOf = projectS
3132
3317
  if (uses.asUsed)
3133
3318
  for (const p of installedPlugins().filter((x) => x.agent === "claude-code" && x.on && x.skills.length)) {
3134
3319
  const name = pluginNameOf(p.id);
3135
- if (p.skills.some((path) => uses.asUsed(`${name}:${basename6(path)}`) > 0)) continue;
3320
+ if (p.skills.some((path) => uses.asUsed(`${name}:${basename7(path)}`) > 0)) continue;
3136
3321
  issues.push({
3137
3322
  id: `plugin-unused:${installId(p)}`,
3138
3323
  severity: "suggestion",
3139
3324
  title: `${p.id}${installedIn(p)}: its skills weren't used in ${plural(uses.days, "day")}`,
3140
- detail: `No Claude Code session used ${p.skills.map((path) => `/${name}:${basename6(path)}`).join(", ")}. Each one's name and description still takes context in every session.${p.extras.length ? ` It also brings ${p.extras.join(", ")}: turning it off pauses those too.` : ""}`,
3325
+ detail: `No Claude Code session used ${p.skills.map((path) => `/${name}:${basename7(path)}`).join(", ")}. Each one's name and description still takes context in every session.${p.extras.length ? ` It also brings ${p.extras.join(", ")}: turning it off pauses those too.` : ""}`,
3141
3326
  choices: [
3142
- { label: `Turn the plugin ${p.id} off`, hint: p.scope === "project" ? `stays installed; edits ${basename6(p.projectPath ?? "the repo")}'s committed .claude/settings.json` : "stays installed", run: () => orThrow(setPluginOn(p, false)) },
3143
- ...p.synced || p.settingsOnly ? [] : [{ label: `Uninstall the plugin ${p.id}`, hint: `its data stays; restorable with skilllib restore${p.scope === "project" ? `; edits ${basename6(p.projectPath ?? "the repo")}'s committed .claude/settings.json` : ""}`, run: () => orThrow(uninstallPlugin(p)) }]
3327
+ { label: `Turn the plugin ${p.id} off`, hint: p.scope === "project" ? `stays installed; edits ${basename7(p.projectPath ?? "the repo")}'s committed .claude/settings.json` : "stays installed", run: () => orThrow(setPluginOn(p, false)) },
3328
+ ...p.synced || p.settingsOnly ? [] : [{ label: `Uninstall the plugin ${p.id}`, hint: `its data stays; restorable with skilllib restore${p.scope === "project" ? `; edits ${basename7(p.projectPath ?? "the repo")}'s committed .claude/settings.json` : ""}`, run: () => orThrow(uninstallPlugin(p)) }]
3144
3329
  ],
3145
3330
  plugin: p.id,
3146
3331
  pluginAt: p.projectPath ?? null
@@ -3193,7 +3378,7 @@ function globalFrom(kind, origin, path) {
3193
3378
  }
3194
3379
  function topLevel(root) {
3195
3380
  try {
3196
- return readdirSync8(root, { withFileTypes: true }).filter((d) => !d.name.startsWith(".") && !BUILD_DIRS.has(d.name)).map((d) => d.isDirectory() ? `${d.name}/` : d.name).sort().slice(0, 60);
3381
+ return readdirSync9(root, { withFileTypes: true }).filter((d) => !d.name.startsWith(".") && !BUILD_DIRS.has(d.name)).map((d) => d.isDirectory() ? `${d.name}/` : d.name).sort().slice(0, 60);
3197
3382
  } catch {
3198
3383
  return [];
3199
3384
  }
@@ -3220,7 +3405,8 @@ function usableHere(root, uses = /* @__PURE__ */ new Map()) {
3220
3405
  state: usual ? void 0 : s.state,
3221
3406
  latest: behind ? s.latest : void 0,
3222
3407
  git: gitState,
3223
- agents: sameSet(loadedBy, agents) ? void 0 : loadedBy
3408
+ agents: sameSet(loadedBy, agents) ? void 0 : loadedBy,
3409
+ personal: s.personal ? true : void 0
3224
3410
  };
3225
3411
  return [JSON.stringify(attrs), s.name, () => ({ ...attrs, skills: [] })];
3226
3412
  })
@@ -3270,7 +3456,7 @@ function firstSentence(description) {
3270
3456
  return sentence.length > 200 ? sentence.slice(0, 199).trimEnd() + "\u2026" : sentence;
3271
3457
  }
3272
3458
  function libraryFor(currentProject) {
3273
- const manifests = visibleProjects().map((p) => ({ name: basename7(p), skills: pinsIn(p) }));
3459
+ const manifests = visibleProjects().map((p) => ({ name: basename8(p), skills: pinsIn(p) }));
3274
3460
  const here = currentProject ? new Set(projectStatus(currentProject).map((s) => s.name)) : null;
3275
3461
  const all = librarySkills();
3276
3462
  const describe2 = (s) => {
@@ -3282,7 +3468,7 @@ function libraryFor(currentProject) {
3282
3468
  }
3283
3469
 
3284
3470
  // src/usage.ts
3285
- import { createReadStream, existsSync as existsSync13, readdirSync as readdirSync9, statSync as statSync5 } from "fs";
3471
+ import { createReadStream, existsSync as existsSync13, readdirSync as readdirSync10, statSync as statSync6 } from "fs";
3286
3472
  import { join as join14 } from "path";
3287
3473
  import { createInterface } from "readline";
3288
3474
  var BUILTIN_COMMANDS = /* @__PURE__ */ new Set([
@@ -3367,17 +3553,17 @@ function transcriptFiles(sinceMs) {
3367
3553
  if (!existsSync13(base)) return [];
3368
3554
  const list2 = (dir) => {
3369
3555
  try {
3370
- return readdirSync9(dir);
3556
+ return readdirSync10(dir);
3371
3557
  } catch {
3372
3558
  return [];
3373
3559
  }
3374
3560
  };
3375
3561
  return list2(base).flatMap((project) => {
3376
3562
  const dir = join14(base, project);
3377
- if (!statSync5(dir, { throwIfNoEntry: false })?.isDirectory()) return [];
3563
+ if (!statSync6(dir, { throwIfNoEntry: false })?.isDirectory()) return [];
3378
3564
  return list2(dir).filter((f) => f.endsWith(".jsonl")).map((f) => join14(dir, f)).filter((f) => {
3379
3565
  try {
3380
- const st = statSync5(f);
3566
+ const st = statSync6(f);
3381
3567
  return st.isFile() && st.mtimeMs >= sinceMs;
3382
3568
  } catch {
3383
3569
  return false;
@@ -3430,11 +3616,11 @@ function usesByProject(uses, projectOf) {
3430
3616
  }
3431
3617
 
3432
3618
  // src/move.ts
3433
- import { basename as basename8 } from "path";
3619
+ import { basename as basename9 } from "path";
3434
3620
  function moveGlobalSkills(skills, roots, {
3435
3621
  group,
3436
3622
  allowTracked = false,
3437
- label = basename8,
3623
+ label = basename9,
3438
3624
  allowTrackedHint = "re-run with --allow-tracked"
3439
3625
  } = {}) {
3440
3626
  const byName = /* @__PURE__ */ new Map();
@@ -3482,6 +3668,8 @@ var FLAGS = {
3482
3668
  json: { type: "boolean" },
3483
3669
  "dry-run": { type: "boolean" },
3484
3670
  "allow-git": { type: "boolean" },
3671
+ personal: { type: "boolean" },
3672
+ shared: { type: "boolean" },
3485
3673
  days: { type: "string" },
3486
3674
  keep: { type: "string" },
3487
3675
  to: { type: "string", multiple: true }
@@ -3489,6 +3677,7 @@ var FLAGS = {
3489
3677
  var VALUE_FLAGS = Object.entries(FLAGS).flatMap(([name, f]) => f.type === "string" ? [`--${name}`] : []);
3490
3678
  function parseArgs(argv) {
3491
3679
  const { values: v, positionals } = parseFlags({ args: argv, options: FLAGS, allowPositionals: true, strict: true });
3680
+ if (v.personal && v.shared) throw new Error("--personal and --shared don't go together: a pin is in one place");
3492
3681
  const days = Number(v.days ?? USAGE_DAYS);
3493
3682
  if (!Number.isInteger(days) || days <= 0) throw new Error(`--days takes a whole number of days, like --days 30 (got ${v.days})`);
3494
3683
  return {
@@ -3502,6 +3691,7 @@ function parseArgs(argv) {
3502
3691
  days,
3503
3692
  dryRun: !!v["dry-run"],
3504
3693
  allowGit: !!v["allow-git"],
3694
+ ...v.personal || v.shared ? { personal: !!v.personal } : {},
3505
3695
  ...v.keep ? { keep: v.keep } : {},
3506
3696
  to: (v.to ?? []).flatMap((t) => t.split(",").filter(Boolean))
3507
3697
  };
@@ -3516,7 +3706,7 @@ function withDirs(root, changes) {
3516
3706
  const { skills } = readManifest(root);
3517
3707
  return changes.map((c) => {
3518
3708
  const dep = c.action !== "removed" && c.action !== "skipped" ? skills[c.name] : void 0;
3519
- return dep ? { ...c, dirs: [dep.dir ?? PROJECT_SKILLS_DIR, ...(dep.links ?? []).filter((d) => !c.inTheWay?.includes(`${d}/${c.name}`))] } : c;
3709
+ return dep ? { ...c, dirs: [dep.dir ?? PROJECT_SKILLS_DIR, ...(dep.links ?? []).filter((d) => !c.inTheWay?.includes(`${d}/${c.name}`))], ...dep.personal ? { personal: true } : {} } : c;
3520
3710
  });
3521
3711
  }
3522
3712
  var incomplete = (c) => c.action === "skipped" || !!c.blocked?.length || !!c.uncommitted?.length || !!c.inTheWay?.length;
@@ -3535,7 +3725,7 @@ function printChanges(root, changes, args) {
3535
3725
  else {
3536
3726
  const icon = c.action === "removed" ? red("-") : c.action === "reset" ? yellow("\u21BA") : c.action === "unchanged" ? dim("=") : green(c.action === "installed" ? "+" : c.action === "relinked" ? "\u21E2" : "\u2191");
3537
3727
  const versions = c.action === "removed" ? "" : c.from && c.from !== c.to ? ` v${c.from} \u2192 v${c.to}` : c.to ? ` v${c.to}` : "";
3538
- info(` ${icon} ${c.name}${versions} ${dim(c.action)}`);
3728
+ info(` ${icon} ${c.name}${versions} ${dim(c.action)}${c.moved ? dim(c.moved === "personal" ? ", now in your personal pins" : ", now in skilllib.json") : ""}`);
3539
3729
  if (c.reason) info(dim(` ${c.reason}`));
3540
3730
  }
3541
3731
  if (c.backedUp) info(dim(` local edits saved to ${tildify(c.backedUp)}; \`skilllib restore ${c.name}\` brings them back`));
@@ -3546,10 +3736,10 @@ function printChanges(root, changes, args) {
3546
3736
  ${done} done, ${skipped} skipped`));
3547
3737
  }
3548
3738
  function projectOfFactory(projects2) {
3549
- const sorted = [...projects2].sort((a, b) => b.length - a.length);
3739
+ const sorted2 = [...projects2].sort((a, b) => b.length - a.length);
3550
3740
  return (cwd) => {
3551
3741
  if (!cwd) return null;
3552
- const known = sorted.find((p) => cwd === p || cwd.startsWith(p + sep8));
3742
+ const known = sorted2.find((p) => cwd === p || cwd.startsWith(p + sep8));
3553
3743
  if (known) return known;
3554
3744
  const repo = cwd.split(`${sep8}.claude${sep8}worktrees${sep8}`)[0];
3555
3745
  return isProjectCandidate(repo) ? repo : null;
@@ -3565,7 +3755,7 @@ var projectsFor = (args) => {
3565
3755
  if (!args.all) return [requireProject()];
3566
3756
  const { readable, unreadable } = readableRoots(visibleProjects());
3567
3757
  for (const u of unreadable) {
3568
- error(`${basename9(u.root)}: ${u.reason}`);
3758
+ error(`${basename10(u.root)}: ${u.reason}`);
3569
3759
  process.exitCode = 1;
3570
3760
  }
3571
3761
  return readable;
@@ -3599,7 +3789,7 @@ async function status(args) {
3599
3789
  const root = requireProject();
3600
3790
  const skills = projectStatus(root);
3601
3791
  const nested = nestedSkills(root);
3602
- info(`${basename9(root)} ${dim(tildify(root))}
3792
+ info(`${basename10(root)} ${dim(tildify(root))}
3603
3793
  `);
3604
3794
  if (skills.length === 0 && nested.length === 0) info("No skills in this project yet. Add one from your library with: skilllib add <name>");
3605
3795
  const usage2 = await usageBySkill(args.days, root);
@@ -3672,7 +3862,7 @@ async function list(args) {
3672
3862
  const installs = /* @__PURE__ */ new Map();
3673
3863
  for (const project of visibleProjects()) {
3674
3864
  for (const name of Object.keys(pinsIn(project))) {
3675
- installs.set(name, [...installs.get(name) ?? [], basename9(project)]);
3865
+ installs.set(name, [...installs.get(name) ?? [], basename10(project)]);
3676
3866
  }
3677
3867
  }
3678
3868
  const usage2 = await usageBySkill(args.days);
@@ -3715,7 +3905,7 @@ function teamStateIn(root, extraDirs = []) {
3715
3905
  function projects(args) {
3716
3906
  const { readable: roots, unreadable } = readableRoots(visibleProjects());
3717
3907
  for (const u of unreadable) {
3718
- error(`${basename9(u.root)}: ${u.reason}`);
3908
+ error(`${basename10(u.root)}: ${u.reason}`);
3719
3909
  process.exitCode = 1;
3720
3910
  }
3721
3911
  if (args.json) {
@@ -3724,7 +3914,7 @@ function projects(args) {
3724
3914
  roots.map((root) => {
3725
3915
  const teamState = teamStateIn(root);
3726
3916
  return {
3727
- name: basename9(root),
3917
+ name: basename10(root),
3728
3918
  path: root,
3729
3919
  skills: projectStatus(root).map((s) => ({ name: s.name, managed: s.managed, state: teamState(s), version: s.version, latest: s.latest }))
3730
3920
  };
@@ -3742,7 +3932,7 @@ function projects(args) {
3742
3932
  const usual = /* @__PURE__ */ new Set(["ok", "local only", "repo skill", "repo skill, in library", "from npx skills"]);
3743
3933
  const attention = skills.filter((s) => !usual.has(teamState(s))).length;
3744
3934
  return {
3745
- Project: basename9(root),
3935
+ Project: basename10(root),
3746
3936
  Path: dim(tildify(root)),
3747
3937
  Skills: skills.map((s) => s.name).join(", ") || dim("none"),
3748
3938
  "Local only": String(skills.filter((s) => teamState(s) === "local only").length),
@@ -3753,10 +3943,15 @@ function projects(args) {
3753
3943
  }
3754
3944
  function add(args) {
3755
3945
  if (args.positional.length === 0) {
3756
- throw new Error("Usage: skilllib add <skill-name>... [--force]");
3946
+ throw new Error("Usage: skilllib add <skill-name>... [--force] [--personal|--shared]");
3757
3947
  }
3758
3948
  const root = requireProject();
3759
- printChanges(root, args.positional.map((name) => addSkill(root, name, { force: args.force, allowTracked: args.allowTracked })), args);
3949
+ const changes = args.positional.map((name) => addSkill(root, name, { force: args.force, allowTracked: args.allowTracked, personal: args.personal }));
3950
+ printChanges(root, changes, args);
3951
+ const pins = readManifest(root).skills;
3952
+ const done = changes.filter((c) => c.action !== "skipped" && pins[c.name]);
3953
+ if (!args.json && done.some((c) => pins[c.name].personal))
3954
+ info(dim(`Pinned in your personal pins (${tildify(personalPinsDir())}) and kept out of git. --shared pins in skilllib.json for the team.`));
3760
3955
  rememberProjects([root]);
3761
3956
  }
3762
3957
  function remove(args) {
@@ -3772,11 +3967,11 @@ function changeEach(args, change, nothing) {
3772
3967
  const changes = change(root);
3773
3968
  if (changes.some(incomplete)) process.exitCode = 1;
3774
3969
  if (!args.json) {
3775
- if (args.all) info(`${basename9(root)} ${dim(tildify(root))}`);
3970
+ if (args.all) info(`${basename10(root)} ${dim(tildify(root))}`);
3776
3971
  if (changes.length === 0) nothing(root);
3777
3972
  else printChanges(root, changes, args);
3778
3973
  }
3779
- return { project: basename9(root), path: root, changes: withDirs(root, changes) };
3974
+ return { project: basename10(root), path: root, changes: withDirs(root, changes) };
3780
3975
  });
3781
3976
  if (args.json) args.all ? json("changes --all", results) : json("changes", results[0].changes);
3782
3977
  return roots;
@@ -3786,7 +3981,7 @@ function sync(args) {
3786
3981
  const roots = changeEach(
3787
3982
  args,
3788
3983
  (root) => syncProject(root, { force: args.force }),
3789
- (root) => existsSync14(join15(root, MANIFEST_FILE)) ? success("Installed versions match skilllib.json") : info(dim("No skilllib.json: nothing to sync"))
3984
+ (root) => hasPins(root) ? success("Installed versions match what's pinned") : info(dim("No skilllib.json or personal pins: nothing to sync"))
3790
3985
  );
3791
3986
  if (!args.all) rememberProjects(roots);
3792
3987
  }
@@ -3807,7 +4002,7 @@ function update(args) {
3807
4002
  function outdated(args) {
3808
4003
  const roots = projectsFor(args);
3809
4004
  const behind = roots.flatMap(
3810
- (root) => projectStatus(root).filter((s) => s.state.endsWith("update available") && s.latest !== null && s.version !== null).map((s) => ({ project: basename9(root), path: root, skill: s.name, installed: s.version, latest: s.latest, state: s.state }))
4005
+ (root) => projectStatus(root).filter((s) => s.state.endsWith("update available") && s.latest !== null && s.version !== null).map((s) => ({ project: basename10(root), path: root, skill: s.name, installed: s.version, latest: s.latest, state: s.state }))
3811
4006
  );
3812
4007
  if (args.json) return json("outdated", behind);
3813
4008
  const rows2 = behind.map((b) => ({ Project: b.project, Skill: b.skill, Installed: `v${b.installed}`, Latest: `v${b.latest}`, Status: colorState(b.state) }));
@@ -3842,7 +4037,7 @@ function importSkills(args) {
3842
4037
  const dep = projectHere(root) === root ? readManifest(root).skills[name] : void 0;
3843
4038
  if (dep && realpathOrNull(join15(root, dep.dir ?? PROJECT_SKILLS_DIR, name)) === realpathOrNull(dir) && dep.hash !== treeHash(dir)) {
3844
4039
  const change = addSkill(root, name);
3845
- if (change.action !== "skipped") info(dim(` ${basename9(root)} now depends on ${name} v${change.to}`));
4040
+ if (change.action !== "skipped") info(dim(` ${basename10(root)} now depends on ${name} v${change.to}`));
3846
4041
  }
3847
4042
  }
3848
4043
  if (args.json) json("import", results);
@@ -3858,14 +4053,14 @@ function scan(args) {
3858
4053
  const library = new Set(librarySkills().map((s) => s.name));
3859
4054
  const skillNames = (root) => [
3860
4055
  // Not a folder that links out of the repo: what's in it isn't the repo's (as in status).
3861
- .../* @__PURE__ */ new Set([...ALL_PROJECT_DIRS.filter((d) => !notWritable(root, d)).flatMap((d) => skillDirsIn(join15(root, d))).map((d) => basename9(d)), ...nestedSkills(root).map((s) => s.name)])
4056
+ .../* @__PURE__ */ new Set([...ALL_PROJECT_DIRS.filter((d) => !notWritable(root, d)).flatMap((d) => skillDirsIn(join15(root, d))).map((d) => basename10(d)), ...nestedSkills(root).map((s) => s.name)])
3862
4057
  ];
3863
4058
  const withSkills = found.filter((root) => skillNames(root).length > 0);
3864
4059
  info(`Scanned ${roots.map((r) => tildify(r)).join(", ")}: ${found.length} project${found.length === 1 ? "" : "s"}, ${withSkills.length} with skills
3865
4060
  `);
3866
4061
  table(
3867
4062
  withSkills.map((root) => ({
3868
- Project: basename9(root),
4063
+ Project: basename10(root),
3869
4064
  Path: dim(tildify(root)),
3870
4065
  Skills: skillNames(root).map((n) => library.has(n) ? n : yellow(n)).join(", ")
3871
4066
  }))
@@ -4035,7 +4230,7 @@ async function usage(args) {
4035
4230
  return json("usage", {
4036
4231
  usageDays: args.days,
4037
4232
  source: "Claude Code transcripts",
4038
- skills: summaries.map((s) => ({ skill: s.skill, uses: s.uses, lastUsed: s.lastUsed, projects: [...s.projects].map((p) => basename9(p)), inLibrary: library.has(s.skill) })),
4233
+ skills: summaries.map((s) => ({ skill: s.skill, uses: s.uses, lastUsed: s.lastUsed, projects: [...s.projects].map((p) => basename10(p)), inLibrary: library.has(s.skill) })),
4039
4234
  unusedLibrarySkills: unused
4040
4235
  });
4041
4236
  }
@@ -4049,7 +4244,7 @@ async function usage(args) {
4049
4244
  summaries.map((s) => ({
4050
4245
  Skill: s.skill,
4051
4246
  Uses: String(s.uses),
4052
- Projects: [...s.projects].map((p) => basename9(p)).join(", ") || dim("-"),
4247
+ Projects: [...s.projects].map((p) => basename10(p)).join(", ") || dim("-"),
4053
4248
  "Last used": lastUsed(s),
4054
4249
  Library: library.has(s.skill) ? green("yes") : dim("no")
4055
4250
  }))
@@ -4073,7 +4268,7 @@ function show(args) {
4073
4268
  path: dir,
4074
4269
  latest: latestVersion(name)?.version ?? null,
4075
4270
  origin: libraryOrigins()[name] ?? "",
4076
- projects: projectsUsing(name).map((p) => basename9(p)),
4271
+ projects: projectsUsing(name).map((p) => basename10(p)),
4077
4272
  skillMd: readFileSync7(join15(dir, "SKILL.md"), "utf-8")
4078
4273
  });
4079
4274
  }
@@ -4105,7 +4300,7 @@ function link(args) {
4105
4300
  const roots = projectsFor(args);
4106
4301
  for (const root of roots) {
4107
4302
  const res = linkAll(root, { allowTracked: args.allowTracked });
4108
- const label = args.all ? `${basename9(root)}: ` : "";
4303
+ const label = args.all ? `${basename10(root)}: ` : "";
4109
4304
  if (res.linked.length) success(`${label}linked ${res.linked.length} skill(s) for your other agents`);
4110
4305
  else if (!args.all && !res.blocked.length && !res.uncommitted.length && !res.inTheWay.length) {
4111
4306
  const missing = projectStatus(root).filter((s) => s.state === "folder missing").map((s) => s.name);
@@ -4151,7 +4346,7 @@ ${unreviewed} not reviewed. Keep one global on purpose: skilllib global keep <na
4151
4346
  }
4152
4347
  function repoRoot(repo) {
4153
4348
  const looksLikePath = /[\\/]/.test(repo) || repo.startsWith(".") || repo.startsWith("~");
4154
- const named = looksLikePath ? [] : visibleProjects().filter((p) => basename9(p) === repo);
4349
+ const named = looksLikePath ? [] : visibleProjects().filter((p) => basename10(p) === repo);
4155
4350
  if (named.length === 1) return named[0];
4156
4351
  if (named.length > 1) throw new Error(`Several projects are named ${repo}: ${named.map(tildify).join(", ")}. Pass its path.`);
4157
4352
  const path = expandHome(repo);
@@ -4177,17 +4372,18 @@ function moveGlobal(args, names, yours) {
4177
4372
  moved,
4178
4373
  ...reason ? { reason } : {},
4179
4374
  ...left ? { left } : {},
4180
- projects: changes.map(([root, c]) => ({ project: basename9(root), path: root, change: withDirs(root, [c])[0] }))
4375
+ projects: changes.map(([root, c]) => ({ project: basename10(root), path: root, change: withDirs(root, [c])[0] }))
4181
4376
  }))
4182
4377
  );
4183
4378
  }
4184
4379
  for (const m of moves) {
4185
- if (m.moved) success(`${m.name} now loads only in ${m.changes.map(([r]) => basename9(r)).join(", ")} (the global copy is in skilllib restore)`);
4380
+ if (m.moved) success(`${m.name} now loads only in ${m.changes.map(([r]) => basename10(r)).join(", ")} (the global copy is in skilllib restore)`);
4186
4381
  else error(`${m.name}: ${m.reason}`);
4187
- for (const [root, c] of m.changes) warnHeldLinks(root, c, `in ${basename9(root)}`);
4382
+ for (const [root, c] of m.changes) warnHeldLinks(root, c, `in ${basename10(root)}`);
4188
4383
  if (m.left) warn(`${m.name}: ${m.left.join(", ")} now point${m.left.length === 1 ? "s" : ""} at nothing (the repo's own copy serves that package): remove ${m.left.length === 1 ? "it" : "them"}`);
4189
4384
  }
4190
- if (moves.some((m) => m.moved)) info(dim("Commit skilllib.json and the skill folders in each repo."));
4385
+ const toCommit = [...new Set(moves.filter((m) => m.moved).flatMap((m) => m.changes.map(([r]) => r)))].filter((r) => existsSync14(join15(r, MANIFEST_FILE)));
4386
+ if (toCommit.length) info(dim(`Commit skilllib.json and the skill folders in ${toCommit.map((r) => basename10(r)).join(", ")}.`));
4191
4387
  }
4192
4388
  function tidy(args) {
4193
4389
  const names = args.positional;
@@ -4200,13 +4396,13 @@ function tidy(args) {
4200
4396
  for (const name of names) {
4201
4397
  const where = targets.map((root) => ({ root, ...foldersOf(root, name) })).filter((w) => w.all.length);
4202
4398
  if (!where.length) {
4203
- problems.push(`${name}: no skill by that name ${args.global ? "in your global folders" : args.all ? "in your projects" : `in ${basename9(targets[0])}`}`);
4399
+ problems.push(`${name}: no skill by that name ${args.global ? "in your global folders" : args.all ? "in your projects" : `in ${basename10(targets[0])}`}`);
4204
4400
  continue;
4205
4401
  }
4206
4402
  if (!args.keep) continue;
4207
4403
  for (const { root, real } of where) {
4208
4404
  if (real.includes(keepFor(root)[name])) continue;
4209
- const label = root ? `${basename9(root)} \xB7 ` : "";
4405
+ const label = root ? `${basename10(root)} \xB7 ` : "";
4210
4406
  problems.push(
4211
4407
  `${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"}`
4212
4408
  );
@@ -4216,7 +4412,7 @@ function tidy(args) {
4216
4412
  throw new Error(problems.join("\n"));
4217
4413
  }
4218
4414
  const reports = targets.map(
4219
- (root) => root === null ? { label: "global folders", report: planGlobalTidy({ keep: keepFor(null) }) } : { label: basename9(root), report: planProjectTidy(root, { keep: keepFor(root) }) }
4415
+ (root) => root === null ? { label: "global folders", report: planGlobalTidy({ keep: keepFor(null) }) } : { label: basename10(root), report: planProjectTidy(root, { keep: keepFor(root) }) }
4220
4416
  );
4221
4417
  let held = 0;
4222
4418
  let changed = 0;