skilllib 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,41 @@
1
1
  // src/output.ts
2
- import { sep } from "path";
2
+ import { resolve as resolve2, sep } from "path";
3
+
4
+ // src/paths.ts
5
+ import { realpathSync } from "fs";
3
6
  import { homedir } from "os";
7
+ import { join, resolve } from "path";
8
+ function userHome() {
9
+ return process.env.HOME || homedir();
10
+ }
11
+ function realPath(path) {
12
+ try {
13
+ return realpathSync(resolve(path));
14
+ } catch {
15
+ return resolve(path);
16
+ }
17
+ }
18
+ function skilllibHome() {
19
+ return process.env.SKILLLIB_HOME ?? join(userHome(), ".skilllib");
20
+ }
21
+ function libraryDir() {
22
+ return join(skilllibHome(), "library");
23
+ }
24
+ function claudeDir() {
25
+ return process.env.CLAUDE_CONFIG_DIR ?? join(userHome(), ".claude");
26
+ }
27
+ function codexHome() {
28
+ return process.env.CODEX_HOME?.trim() || join(userHome(), ".codex");
29
+ }
30
+ function codexSystemDir() {
31
+ return process.env.SKILLLIB_CODEX_SYSTEM_DIR ?? "/etc/codex/skills";
32
+ }
33
+ var PROJECT_SKILLS_DIR = ".claude/skills";
34
+ var AGENTS_SKILLS_DIR = ".agents/skills";
35
+ var MANIFEST_FILE = "skilllib.json";
36
+ var OWN_SKILL_MARKER = ".managed-by-skilllib";
37
+
38
+ // src/output.ts
4
39
  var jsonMode = false;
5
40
  function setJsonMode(on) {
6
41
  jsonMode = on;
@@ -19,8 +54,9 @@ var success = (msg) => out(`${green("\u2713")} ${msg}`);
19
54
  var warn = (msg) => out(`${yellow("!")} ${msg}`);
20
55
  var error = (msg) => console.error(`${red("\u2717")} ${msg}`);
21
56
  function tildify(path) {
22
- const home2 = homedir();
23
- return path === home2 || path.startsWith(home2 + sep) ? "~" + path.slice(home2.length) : path;
57
+ for (const home2 of /* @__PURE__ */ new Set([resolve2(userHome()), realPath(userHome())]))
58
+ if (path === home2 || path.startsWith(home2 + sep)) return ("~" + path.slice(home2.length)).replace(/\\/g, "/");
59
+ return path;
24
60
  }
25
61
  function truncate(s, max) {
26
62
  const line = s.replace(/\s+/g, " ");
@@ -41,35 +77,9 @@ import { existsSync as existsSync8, lstatSync as lstatSync3, mkdirSync as mkdirS
41
77
  import { join as join9 } from "path";
42
78
 
43
79
  // src/config.ts
44
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
45
- import { homedir as homedir4 } from "os";
46
- import { join as join6, resolve as resolve2 } from "path";
47
-
48
- // src/paths.ts
80
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
49
81
  import { homedir as homedir2 } from "os";
50
- import { join } from "path";
51
- function userHome() {
52
- return process.env.HOME || homedir2();
53
- }
54
- function skilllibHome() {
55
- return process.env.SKILLLIB_HOME ?? join(userHome(), ".skilllib");
56
- }
57
- function libraryDir() {
58
- return join(skilllibHome(), "library");
59
- }
60
- function claudeDir() {
61
- return process.env.CLAUDE_CONFIG_DIR ?? join(userHome(), ".claude");
62
- }
63
- function codexHome() {
64
- return process.env.CODEX_HOME?.trim() || join(userHome(), ".codex");
65
- }
66
- function codexSystemDir() {
67
- return process.env.SKILLLIB_CODEX_SYSTEM_DIR ?? "/etc/codex/skills";
68
- }
69
- var PROJECT_SKILLS_DIR = ".claude/skills";
70
- var AGENTS_SKILLS_DIR = ".agents/skills";
71
- var MANIFEST_FILE = "skilllib.json";
72
- var OWN_SKILL_MARKER = ".managed-by-skilllib";
82
+ import { join as join6, resolve as resolve4 } from "path";
73
83
 
74
84
  // src/harnesses.ts
75
85
  import { existsSync } from "fs";
@@ -155,8 +165,7 @@ function installDirs(enabled, keep) {
155
165
 
156
166
  // src/project.ts
157
167
  import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
158
- import { homedir as homedir3 } from "os";
159
- import { dirname, join as join5, resolve } from "path";
168
+ import { dirname, join as join5, resolve as resolve3 } from "path";
160
169
 
161
170
  // src/versions.ts
162
171
  import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
@@ -164,7 +173,7 @@ import { join as join4 } from "path";
164
173
 
165
174
  // src/skills.ts
166
175
  import { createHash } from "crypto";
167
- import { cpSync, existsSync as existsSync2, readdirSync, readFileSync, realpathSync, rmSync, statSync } from "fs";
176
+ import { cpSync, existsSync as existsSync2, readdirSync, readFileSync, realpathSync as realpathSync2, rmSync, statSync } from "fs";
168
177
  import { basename, join as join3, relative, sep as sep2 } from "path";
169
178
  import { parse as parseYaml } from "yaml";
170
179
  var IGNORED = /* @__PURE__ */ new Set([".DS_Store", ".git", "node_modules"]);
@@ -204,7 +213,7 @@ function treeHash(dir) {
204
213
  }
205
214
  function copySkill(from, to) {
206
215
  rmSync(to, { recursive: true, force: true });
207
- cpSync(realpathSync(from), to, { recursive: true, dereference: true, filter: (src) => !IGNORED.has(basename(src)) });
216
+ cpSync(realpathSync2(from), to, { recursive: true, dereference: true, filter: (src) => !IGNORED.has(basename(src)) });
208
217
  }
209
218
  function skillDirsIn(dir) {
210
219
  if (!existsSync2(dir)) return [];
@@ -263,19 +272,17 @@ function getVersion(name, version) {
263
272
 
264
273
  // src/project.ts
265
274
  function findProjectRoot(from = process.cwd()) {
266
- let dir = resolve(from);
275
+ let dir = resolve3(from);
267
276
  while (true) {
268
277
  if (existsSync4(join5(dir, MANIFEST_FILE)) || existsSync4(join5(dir, ".git"))) return dir;
269
278
  const parent = dirname(dir);
270
- if (parent === dir) return resolve(from);
279
+ if (parent === dir) return resolve3(from);
271
280
  dir = parent;
272
281
  }
273
282
  }
274
- function projectSkillsDir(root) {
275
- return join5(root, PROJECT_SKILLS_DIR);
276
- }
277
- function agentsSkillsDir(root) {
278
- return join5(root, AGENTS_SKILLS_DIR);
283
+ function projectHere(from = process.cwd()) {
284
+ const root = findProjectRoot(from);
285
+ return isProjectCandidate(root) && (existsSync4(join5(root, ".git")) || existsSync4(join5(root, MANIFEST_FILE))) ? root : null;
279
286
  }
280
287
  function readManifest(root) {
281
288
  const path = join5(root, MANIFEST_FILE);
@@ -315,14 +322,23 @@ function knownProjects() {
315
322
  const path = projectsFile();
316
323
  if (!existsSync4(path)) return [];
317
324
  const list2 = JSON.parse(readFileSync3(path, "utf-8"));
318
- return list2.filter((p) => existsSync4(p) && isProjectCandidate(p));
325
+ return onePerFolder(list2.filter((p) => existsSync4(p) && isProjectCandidate(p)));
326
+ }
327
+ function onePerFolder(paths) {
328
+ const byReal = /* @__PURE__ */ new Map();
329
+ for (const p of paths) {
330
+ const real = realPath(p);
331
+ const seen = byReal.get(real);
332
+ if (seen === void 0 || seen === real && p !== real) byReal.set(real, p);
333
+ }
334
+ return [...new Set(byReal.values())];
319
335
  }
320
336
  function isProjectCandidate(root) {
321
- return resolve(root) !== homedir3();
337
+ return realPath(root) !== realPath(userHome());
322
338
  }
323
339
  function rememberProjects(roots) {
324
- const incoming = roots.map((r) => resolve(r)).filter(isProjectCandidate);
325
- const all = [.../* @__PURE__ */ new Set([...knownProjects(), ...incoming])].sort();
340
+ const incoming = roots.map((r) => resolve3(r)).filter(isProjectCandidate);
341
+ const all = onePerFolder([...knownProjects(), ...incoming]).sort();
326
342
  mkdirSync2(skilllibHome(), { recursive: true });
327
343
  writeFileSync2(projectsFile(), JSON.stringify(all, null, 2) + "\n");
328
344
  }
@@ -344,10 +360,12 @@ function writeConfig(config) {
344
360
  writeFileSync3(configFile(), JSON.stringify(config, null, 2) + "\n");
345
361
  }
346
362
  function expandHome(path) {
347
- return resolve2(path.replace(/^~(?=$|\/)/, homedir4()));
363
+ return resolve4(path.replace(/^~(?=$|\/)/, homedir2()));
348
364
  }
349
365
  function addRoot(path) {
350
366
  const root = expandHome(path);
367
+ if (!existsSync5(root)) throw new Error(`${tildify(root)} doesn't exist`);
368
+ if (!statSync2(root).isDirectory()) throw new Error(`${tildify(root)} isn't a folder`);
351
369
  const config = readConfig();
352
370
  if (!config.roots.includes(root)) writeConfig({ ...config, roots: [...config.roots, root].sort() });
353
371
  return root;
@@ -361,8 +379,10 @@ function removeRoot(path) {
361
379
  }
362
380
  function setHidden(project, hidden) {
363
381
  const config = readConfig();
382
+ if (config.hidden.includes(project) === hidden) return false;
364
383
  const rest = config.hidden.filter((h) => h !== project);
365
384
  writeConfig({ ...config, hidden: hidden ? [...rest, project].sort() : rest });
385
+ return true;
366
386
  }
367
387
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "vendor", "target", "Library", "Applications", "Pictures", "Music", "Movies"]);
368
388
  function findProjects(dir, depth = 4) {
@@ -386,8 +406,8 @@ function discoverProjects() {
386
406
  return visibleProjects();
387
407
  }
388
408
  function visibleProjects() {
389
- const hidden = new Set(readConfig().hidden);
390
- return knownProjects().filter((p) => !hidden.has(p));
409
+ const hidden = new Set(readConfig().hidden.map(realPath));
410
+ return knownProjects().filter((p) => !hidden.has(realPath(p)));
391
411
  }
392
412
  function enabledHarnesses() {
393
413
  return readConfig().harnesses ?? HARNESSES.filter((h) => h.installed()).map((h) => h.id);
@@ -418,12 +438,12 @@ function setKeepGlobal(names, keep) {
418
438
 
419
439
  // src/library.ts
420
440
  import { execFileSync } from "child_process";
421
- import { existsSync as existsSync7, lstatSync as lstatSync2, mkdirSync as mkdirSync5, readdirSync as readdirSync4, readFileSync as readFileSync6, realpathSync as realpathSync3, renameSync, rmSync as rmSync2, symlinkSync, unlinkSync, writeFileSync as writeFileSync5 } from "fs";
441
+ import { existsSync as existsSync7, lstatSync as lstatSync2, mkdirSync as mkdirSync5, readdirSync as readdirSync4, readFileSync as readFileSync6, realpathSync as realpathSync4, renameSync, rmSync as rmSync2, symlinkSync, unlinkSync, writeFileSync as writeFileSync5 } from "fs";
422
442
  import { basename as basename3, dirname as dirname2, join as join8, relative as relative2, sep as sep3 } from "path";
423
443
 
424
444
  // src/sources.ts
425
- import { existsSync as existsSync6, lstatSync, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync5, readlinkSync, realpathSync as realpathSync2, writeFileSync as writeFileSync4 } from "fs";
426
- import { basename as basename2, join as join7, resolve as resolve3 } from "path";
445
+ import { existsSync as existsSync6, lstatSync, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync5, readlinkSync, realpathSync as realpathSync3, writeFileSync as writeFileSync4 } from "fs";
446
+ import { basename as basename2, join as join7, resolve as resolve5 } from "path";
427
447
  function readJson(path) {
428
448
  try {
429
449
  return JSON.parse(readFileSync5(path, "utf-8"));
@@ -432,24 +452,36 @@ function readJson(path) {
432
452
  }
433
453
  }
434
454
  function skillsShLock() {
435
- return readJson(join7(resolve3(claudeDir(), ".."), ".agents", ".skill-lock.json"))?.skills ?? {};
455
+ return readJson(join7(userHome(), ".agents", ".skill-lock.json"))?.skills ?? {};
436
456
  }
437
457
  function projectSkillsLock(root) {
438
458
  return readJson(join7(root, "skills-lock.json"))?.skills ?? {};
439
459
  }
440
460
  function realpathOrNull(path) {
441
461
  try {
442
- return realpathSync2(path);
462
+ return realpathSync3(path);
443
463
  } catch {
444
464
  return null;
445
465
  }
446
466
  }
447
467
  function harnessesReading(dir, enabled) {
448
- return enabled.filter((id) => HARNESSES.find((h) => h.id === id)?.globalDirs().includes(dir));
468
+ const real = realPath(dir);
469
+ return enabled.filter((id) => HARNESSES.find((h) => h.id === id)?.globalDirs().some((d) => realPath(d) === real));
449
470
  }
450
471
  var systemDirs = () => [codexSystemDir()];
451
472
  function globalSkillDirs() {
452
- return [...new Set(HARNESSES.flatMap((h) => h.globalDirs()))].filter((d) => !systemDirs().includes(d));
473
+ const seen = /* @__PURE__ */ new Set();
474
+ return [...new Set(HARNESSES.flatMap((h) => h.globalDirs()))].filter((d) => {
475
+ if (systemDirs().includes(d)) return false;
476
+ const real = realPath(d);
477
+ if (seen.has(real)) return false;
478
+ seen.add(real);
479
+ return true;
480
+ });
481
+ }
482
+ function globalDirAs(dir) {
483
+ const real = realPath(dir);
484
+ return globalSkillDirs().find((d) => realPath(d) === real) ?? dir;
453
485
  }
454
486
  function systemSkills(enabled) {
455
487
  return systemDirs().flatMap(
@@ -492,7 +524,7 @@ function globalFolderSkills(enabled) {
492
524
  const inAgents = e.dir === agentsDir || target_.includes(".agents/skills");
493
525
  const locked = lock[e.name]?.source;
494
526
  if (existsSync6(join7(e.path, OWN_SKILL_MARKER))) {
495
- skills.push({ name: e.name, kind: "built-in", origin: "skilllib", path: e.path, description: readSkillInfo(e.path).description, movable: false, broken: false, harnesses: harnessesReading(e.dir, enabled), links: [] });
527
+ skills.push({ name: e.name, kind: "skilllib", origin: "skilllib", path: e.path, description: readSkillInfo(e.path).description, movable: false, broken: false, harnesses: harnessesReading(e.dir, enabled), links: [] });
496
528
  continue;
497
529
  }
498
530
  skills.push({
@@ -516,8 +548,7 @@ function globalFolderSkills(enabled) {
516
548
  return skills;
517
549
  }
518
550
  function tildify2(p) {
519
- const shown = p.startsWith(userHome()) ? "~" + p.slice(userHome().length) : p;
520
- return shown.replace(/\\/g, "/");
551
+ return tildify(p).replace(/\\/g, "/");
521
552
  }
522
553
  function cursorBuiltInSkills(enabled) {
523
554
  if (!enabled.includes("cursor")) return [];
@@ -637,7 +668,7 @@ function recordOrigin(name, origin) {
637
668
  writeFileSync4(originsFile(), JSON.stringify({ ...origins, [name]: origin }, null, 2) + "\n");
638
669
  }
639
670
  function originFor(dir) {
640
- const match = machineSkills().find((s) => resolve3(s.path) === resolve3(dir));
671
+ const match = machineSkills().find((s) => resolve5(s.path) === resolve5(dir));
641
672
  if (match) return match.kind === "global" ? "global folder" : `${match.kind}: ${match.origin}`;
642
673
  const project = dir.split(/[\\/]\.(?:claude|agents)[\\/]skills[\\/]/)[0];
643
674
  if (!project || project === dir) return dir;
@@ -658,7 +689,7 @@ function librarySkillDir(name) {
658
689
  }
659
690
  function realpathOrNull2(path) {
660
691
  try {
661
- return realpathSync3(path);
692
+ return realpathSync4(path);
662
693
  } catch {
663
694
  return null;
664
695
  }
@@ -797,20 +828,40 @@ function nestedSkills(root, { depth = 3, enabled = enabledHarnesses() } = {}) {
797
828
  walk(root, depth);
798
829
  return found.sort((a, b) => a.location.localeCompare(b.location) || a.name.localeCompare(b.name));
799
830
  }
800
- function linkEverywhere(root, name, location, { allowTracked = false } = {}) {
831
+ function sharedByGit(root, location, name) {
832
+ const copy = `${location}/${name}`;
833
+ if (isGitTracked(root, copy)) return true;
834
+ if (!isGitTracked(root, location)) return false;
835
+ try {
836
+ execFileSync("git", ["check-ignore", "-q", "--no-index", "--", copy], { cwd: root, stdio: "ignore" });
837
+ return false;
838
+ } catch {
839
+ return true;
840
+ }
841
+ }
842
+ function linkEverywhere(root, name, location, { allowTracked = false, dryRun = false } = {}) {
801
843
  const enabled = enabledHarnesses();
802
- const result = { created: [], blocked: [] };
844
+ const result = { created: [], blocked: [], uncommitted: [] };
845
+ let shared;
803
846
  for (const id of enabled) {
804
847
  if (visibilityOf(root, name, [id])[0].paths > 0) continue;
805
848
  const dir = harness(id).projectDirs[0];
806
849
  const link2 = join8(root, dir, name);
807
- if (entryExists(link2)) continue;
808
- if (!allowTracked && isGitTracked(root, dir) && !readConfig().agentsDirOk?.includes(root)) {
809
- result.blocked.push(dir);
810
- continue;
850
+ if (entryExists(link2) || Object.values(result).some((dirs) => dirs.includes(dir))) continue;
851
+ if (isGitTracked(root, dir)) {
852
+ if (!(shared ??= sharedByGit(root, location, name))) {
853
+ if (!result.uncommitted.includes(dir)) result.uncommitted.push(dir);
854
+ continue;
855
+ }
856
+ if (!allowTracked && !readConfig().agentsDirOk?.includes(root)) {
857
+ if (!result.blocked.includes(dir)) result.blocked.push(dir);
858
+ continue;
859
+ }
860
+ }
861
+ if (!dryRun) {
862
+ mkdirSync5(join8(root, dir), { recursive: true });
863
+ linkDir(join8(root, location, name), link2);
811
864
  }
812
- mkdirSync5(join8(root, dir), { recursive: true });
813
- linkDir(join8(root, location, name), link2);
814
865
  result.created.push(dir);
815
866
  }
816
867
  return result;
@@ -826,7 +877,7 @@ function unlinkEverywhere(root, name) {
826
877
  function relinkDependency(root, name, { allowTracked = false } = {}) {
827
878
  const manifest = readManifest(root);
828
879
  const dep = manifest.skills[name];
829
- if (!dep) return { created: [], blocked: [] };
880
+ if (!dep) return { created: [], blocked: [], uncommitted: [] };
830
881
  const result = linkEverywhere(root, name, dep.dir ?? PROJECT_SKILLS_DIR, { allowTracked });
831
882
  if (result.created.length) {
832
883
  manifest.skills[name] = { ...dep, links: [.../* @__PURE__ */ new Set([...dep.links ?? [], ...result.created])] };
@@ -837,13 +888,15 @@ function relinkDependency(root, name, { allowTracked = false } = {}) {
837
888
  function linkAll(root, { allowTracked = false } = {}) {
838
889
  const linked = [];
839
890
  const blocked = /* @__PURE__ */ new Set();
891
+ const uncommitted = [];
840
892
  for (const skill of projectStatus(root)) {
841
893
  if (!skill.visibility.some((v) => v.paths === 0) || skill.state === "folder missing") continue;
842
894
  const res = skill.managed ? relinkDependency(root, skill.name, { allowTracked }) : linkEverywhere(root, skill.name, skill.location, { allowTracked });
843
895
  if (res.created.length) linked.push({ name: skill.name, into: res.created });
844
896
  for (const dir of res.blocked) blocked.add(dir);
897
+ if (res.uncommitted.length) uncommitted.push(skill.name);
845
898
  }
846
- return { linked, blocked: [...blocked] };
899
+ return { linked, blocked: [...blocked], uncommitted };
847
900
  }
848
901
  function addSkill(root, name, { force = false, version, allowTracked = false } = {}) {
849
902
  const target = version === void 0 ? latestVersion(name) : getVersion(name, version);
@@ -861,13 +914,17 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
861
914
  reason: recorded ? "has local edits (use --force to overwrite, or save them to the library first)" : `${primary}/${name} already exists with different content (use --force to replace it)`
862
915
  };
863
916
  }
917
+ let backedUp;
864
918
  if (local !== target.hash || isLink(to)) {
865
919
  mkdirSync5(join8(root, primary), { recursive: true });
866
920
  if (isLink(to)) unlinkSync(to);
921
+ else if (local !== null && !knownContent) backedUp = stash(to, "edit-backup");
867
922
  copySkill(versionDir(name, target.version), to);
868
923
  }
869
924
  const links = [];
870
925
  const blocked = [];
926
+ const uncommitted = [];
927
+ let shared;
871
928
  for (const dir of linkDirs) {
872
929
  const link2 = join8(root, dir, name);
873
930
  if (isLink(link2) && realpathOrNull2(link2) === realpathOrNull2(to)) {
@@ -875,9 +932,15 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
875
932
  continue;
876
933
  }
877
934
  if (entryExists(link2)) continue;
878
- if (!allowTracked && !recorded?.links?.includes(dir) && isGitTracked(root, dir) && !readConfig().agentsDirOk?.includes(root)) {
879
- blocked.push(dir);
880
- continue;
935
+ if (!recorded?.links?.includes(dir) && isGitTracked(root, dir)) {
936
+ if (!(shared ??= sharedByGit(root, primary, name))) {
937
+ uncommitted.push(dir);
938
+ continue;
939
+ }
940
+ if (!allowTracked && !readConfig().agentsDirOk?.includes(root)) {
941
+ blocked.push(dir);
942
+ continue;
943
+ }
881
944
  }
882
945
  mkdirSync5(join8(root, dir), { recursive: true });
883
946
  linkDir(to, link2);
@@ -892,10 +955,12 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
892
955
  writeManifest(root, manifest);
893
956
  return {
894
957
  name,
895
- action: recorded || local !== null ? "updated" : "installed",
958
+ action: !recorded && local === null ? "installed" : backedUp && recorded?.version === target.version ? "reset" : "updated",
896
959
  ...recorded ? { from: recorded.version } : {},
897
960
  to: target.version,
898
- ...blocked.length ? { blocked } : {}
961
+ ...blocked.length ? { blocked } : {},
962
+ ...uncommitted.length ? { uncommitted } : {},
963
+ ...backedUp ? { backedUp } : {}
899
964
  };
900
965
  }
901
966
  function removeSkill(root, name, { force = false } = {}) {
@@ -904,17 +969,17 @@ function removeSkill(root, name, { force = false } = {}) {
904
969
  if (!recorded) return { name, action: "skipped", reason: "not managed by skilllib in this project" };
905
970
  const dir = join8(root, recorded.dir ?? PROJECT_SKILLS_DIR, name);
906
971
  const local = treeHash(dir);
907
- if (local !== null && local !== recorded.hash && !versionForHash(name, local) && !force) {
908
- return { name, action: "skipped", reason: "has local edits (use --force to delete anyway)" };
909
- }
972
+ const edited = local !== null && local !== recorded.hash && !versionForHash(name, local);
973
+ if (edited && !force) return { name, action: "skipped", reason: "has local edits (use --force to delete anyway)" };
910
974
  for (const linkDir2 of recorded.links ?? []) {
911
975
  const link2 = join8(root, linkDir2, name);
912
976
  if (isLink(link2)) unlinkSync(link2);
913
977
  }
914
- rmSync2(dir, { recursive: true, force: true });
978
+ const backedUp = edited ? stash(dir, "edit-backup") : void 0;
979
+ if (!backedUp) rmSync2(dir, { recursive: true, force: true });
915
980
  delete manifest.skills[name];
916
981
  writeManifest(root, manifest);
917
- return { name, action: "removed" };
982
+ return { name, action: "removed", ...backedUp ? { backedUp } : {} };
918
983
  }
919
984
  function syncProject(root, { force = false } = {}) {
920
985
  const { skills } = readManifest(root);
@@ -925,7 +990,8 @@ function syncProject(root, { force = false } = {}) {
925
990
  const pinned = dep.version > 0 && getVersion(name, dep.version) ? dep.version : void 0;
926
991
  if (local !== null && local !== dep.hash && !force) return [{ name, action: "skipped", reason: "has local edits (use --force to reset them)" }];
927
992
  const change = addSkill(root, name, { force: true, version: pinned });
928
- return change.action === "skipped" ? [change] : [{ ...change, action: local === null ? "installed" : "updated" }];
993
+ if (change.action === "skipped") return [{ ...change, reason: `not in your library (${tildify(libraryDir())}); import it, or set SKILLLIB_HOME to the library that has it` }];
994
+ return [{ ...change, action: local === null ? "installed" : change.action }];
929
995
  });
930
996
  }
931
997
  function discardEdits(root, name) {
@@ -944,6 +1010,7 @@ function updateProject(root, names, { force = false } = {}) {
944
1010
  const dep = skills[name];
945
1011
  const local = treeHash(join8(root, dep.dir ?? PROJECT_SKILLS_DIR, name));
946
1012
  if (dep.hash === latest.hash && local === latest.hash) return [];
1013
+ if (dep.hash === latest.hash && local !== null && !force) return [];
947
1014
  const change = addSkill(root, name, { force });
948
1015
  return change.action === "skipped" || change.from !== change.to || local !== latest.hash ? [change] : [];
949
1016
  });
@@ -1038,12 +1105,12 @@ function createSkill(name, description) {
1038
1105
  join8(dir, "SKILL.md"),
1039
1106
  `---
1040
1107
  name: ${name}
1041
- description: ${description || "Describe when Claude should use this skill."}
1108
+ description: ${description || "Describe when an agent should use this skill."}
1042
1109
  ---
1043
1110
 
1044
1111
  # ${name}
1045
1112
 
1046
- Instructions for Claude go here.
1113
+ Instructions for the agent go here.
1047
1114
  `
1048
1115
  );
1049
1116
  forgetLatest(name);
@@ -1061,7 +1128,11 @@ function listBackups() {
1061
1128
  const from = index[entry] ?? (kind === "trash" ? librarySkillDir(name) : join8(claudeDir(), "skills", name));
1062
1129
  return [{ name, kind, path: join8(dir, entry), movedAt: `${match[2]} ${match[3]}:${match[4]}`, from }];
1063
1130
  });
1064
- }).sort((a, b) => b.movedAt.localeCompare(a.movedAt));
1131
+ }).sort((a, b) => b.path.slice(-24).localeCompare(a.path.slice(-24)));
1132
+ }
1133
+ function backupFrom(backup) {
1134
+ if (backup.kind === "plugin") return "Claude Code plugin";
1135
+ return backup.from === librarySkillDir(backup.name) ? "library (deleted)" : tildify(dirname2(backup.from));
1065
1136
  }
1066
1137
  function restoreBackup(backup) {
1067
1138
  const to = backup.from;
@@ -1114,7 +1185,7 @@ function installAgentSkill(enabled = enabledHarnesses()) {
1114
1185
  const [primary, ...links] = agentSkillDirs(enabled);
1115
1186
  if (!primary) return { ok: false, reason: "no agents chosen yet (skilllib harnesses <id>...)" };
1116
1187
  const foreign = [primary, ...links].find((d) => entryExists2(d) && !isOurs(d) && !isDangling(d));
1117
- if (foreign) return { ok: false, reason: `${foreign} already exists and isn't skilllib's` };
1188
+ if (foreign) return { ok: false, reason: `${tildify(foreign)} already exists and isn't skilllib's` };
1118
1189
  for (const d of [primary, ...links]) if (isDangling(d)) unlinkSync2(d);
1119
1190
  mkdirSync6(primary, { recursive: true });
1120
1191
  writeFileSync6(join9(primary, "SKILL.md"), shippedSkill());
@@ -1143,12 +1214,8 @@ function refreshAgentSkill() {
1143
1214
  }
1144
1215
 
1145
1216
  // src/commands.ts
1146
- import { existsSync as existsSync14, readFileSync as readFileSync9 } from "fs";
1147
- import { basename as basename8, dirname as dirname5, join as join15, resolve as resolve4, sep as sep5 } from "path";
1148
-
1149
- // src/here.ts
1150
- import { existsSync as existsSync12, readdirSync as readdirSync7 } from "fs";
1151
- import { basename as basename7, join as join13 } from "path";
1217
+ import { existsSync as existsSync13, readFileSync as readFileSync9 } from "fs";
1218
+ import { basename as basename8, join as join14, resolve as resolve7, sep as sep5 } from "path";
1152
1219
 
1153
1220
  // src/git.ts
1154
1221
  import { execFileSync as execFileSync2 } from "child_process";
@@ -1162,9 +1229,9 @@ function git(root, args, input) {
1162
1229
  return typeof out2 === "string" ? out2 : null;
1163
1230
  }
1164
1231
  }
1165
- function gitInfo(root) {
1232
+ function gitInfo(root, extraDirs = []) {
1166
1233
  if (!existsSync9(join10(root, ".git"))) return null;
1167
- const dirs = [...ALL_PROJECT_DIRS, MANIFEST_FILE].filter((d) => existsSync9(join10(root, d)));
1234
+ const dirs = [...ALL_PROJECT_DIRS, MANIFEST_FILE, ...extraDirs].filter((d) => existsSync9(join10(root, d)));
1168
1235
  if (dirs.length === 0) return { of: () => "new" };
1169
1236
  const tracked = (git(root, ["ls-files", "-z", "--", ...dirs]) ?? "").split("\0").filter(Boolean);
1170
1237
  const dirty = (git(root, ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...dirs]) ?? "").split("\0").filter(Boolean).map((entry) => entry.slice(3));
@@ -1189,14 +1256,18 @@ function relativeTo(root, path) {
1189
1256
  return relative3(root, path).split("\\").join("/");
1190
1257
  }
1191
1258
 
1259
+ // src/here.ts
1260
+ import { readdirSync as readdirSync7 } from "fs";
1261
+ import { basename as basename7 } from "path";
1262
+
1192
1263
  // src/health.ts
1193
1264
  import { lstatSync as lstatSync4 } from "fs";
1194
- import { basename as basename6 } from "path";
1265
+ import { basename as basename6, dirname as dirname5 } from "path";
1195
1266
 
1196
1267
  // src/plugins.ts
1197
1268
  import { execFileSync as execFileSync3 } from "child_process";
1198
- import { existsSync as existsSync10, mkdirSync as mkdirSync7, readdirSync as readdirSync5, readFileSync as readFileSync8, statSync as statSync2, writeFileSync as writeFileSync7 } from "fs";
1199
- import { basename as basename4, dirname as dirname3, join as join11 } from "path";
1269
+ import { existsSync as existsSync10, mkdirSync as mkdirSync7, readdirSync as readdirSync5, readFileSync as readFileSync8, statSync as statSync3, writeFileSync as writeFileSync7 } from "fs";
1270
+ import { basename as basename4, dirname as dirname3, join as join11, resolve as resolve6 } from "path";
1200
1271
  function readJson2(path) {
1201
1272
  try {
1202
1273
  return JSON.parse(readFileSync8(path, "utf-8"));
@@ -1249,8 +1320,7 @@ function claudeBinary() {
1249
1320
  }
1250
1321
  }) ?? null;
1251
1322
  }
1252
- function claude(args, cwd) {
1253
- const bin = claudeBinary();
1323
+ function claude(args, cwd, bin = claudeBinary()) {
1254
1324
  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` };
1255
1325
  try {
1256
1326
  execFileSync3(bin, args, { cwd, stdio: ["ignore", "pipe", "pipe"], timeout: 12e4, encoding: "utf-8" });
@@ -1271,9 +1341,44 @@ function setPluginIn(file, id, on) {
1271
1341
  function turnOffSynced(id) {
1272
1342
  return setPluginIn(join11(claudeDir(), "settings.json"), id, false);
1273
1343
  }
1344
+ function ignoreLocalSettings(root) {
1345
+ const file = ".claude/settings.local.json";
1346
+ const git2 = (...args) => execFileSync3("git", args, { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1347
+ try {
1348
+ git2("rev-parse", "--git-dir");
1349
+ } catch {
1350
+ return "";
1351
+ }
1352
+ try {
1353
+ git2("ls-files", "--error-unmatch", "--", file);
1354
+ return "; git tracks that file, so this shows in git status";
1355
+ } catch {
1356
+ }
1357
+ try {
1358
+ git2("check-ignore", "-q", "--", file);
1359
+ return ", which git ignores";
1360
+ } catch {
1361
+ }
1362
+ try {
1363
+ const exclude = resolve6(root, git2("rev-parse", "--git-path", "info/exclude"));
1364
+ const before = existsSync10(exclude) ? readFileSync8(exclude, "utf-8") : "";
1365
+ mkdirSync7(dirname3(exclude), { recursive: true });
1366
+ writeFileSync7(exclude, `${before}${before && !before.endsWith("\n") ? "\n" : ""}/${git2("rev-parse", "--show-prefix")}${file}
1367
+ `);
1368
+ return ", which git now ignores via .git/info/exclude";
1369
+ } catch {
1370
+ return "; git doesn't ignore that file yet: it's local only, don't commit it";
1371
+ }
1372
+ }
1274
1373
  function turnOffIn(plugin, root) {
1275
- const r = plugin.synced ? setPluginIn(join11(root, ".claude", "settings.local.json"), plugin.id, false) : claude(["plugin", "disable", plugin.id, "--scope", "local"], root);
1276
- return r.ok ? { ok: true, message: `${plugin.id} turned off in ${basename4(root)}; other repos keep it` } : { ok: false, message: `${plugin.id}: ${r.reason}` };
1374
+ const done = `${plugin.id} turned off in ${basename4(root)}; other repos keep it`;
1375
+ const bin = plugin.synced ? null : claudeBinary();
1376
+ if (!bin) {
1377
+ const r2 = setPluginIn(join11(root, ".claude", "settings.local.json"), plugin.id, false);
1378
+ return r2.ok ? { ok: true, message: `${done} (in .claude/settings.local.json${ignoreLocalSettings(root)})` } : { ok: false, message: `${plugin.id}: ${r2.reason}` };
1379
+ }
1380
+ const r = claude(["plugin", "disable", plugin.id, "--scope", "local"], root, bin);
1381
+ return r.ok ? { ok: true, message: done } : { ok: false, message: `${plugin.id}: ${r.reason}` };
1277
1382
  }
1278
1383
  function removedFile() {
1279
1384
  return join11(skilllibHome(), "plugin-backup.json");
@@ -1315,11 +1420,11 @@ function restorePlugin(backup) {
1315
1420
  }
1316
1421
  function cursorPluginSkills() {
1317
1422
  const cache = join11(userHome(), ".cursor", "plugins", "cache");
1318
- const dirs = (p) => existsSync10(p) ? readdirSync5(p).filter((d) => !d.startsWith(".") && statSync2(join11(p, d)).isDirectory()) : [];
1423
+ const dirs = (p) => existsSync10(p) ? readdirSync5(p).filter((d) => !d.startsWith(".") && statSync3(join11(p, d)).isDirectory()) : [];
1319
1424
  return dirs(cache).flatMap(
1320
1425
  (marketplace) => dirs(join11(cache, marketplace)).flatMap((plugin) => {
1321
1426
  const versions = dirs(join11(cache, marketplace, plugin)).map((v) => join11(cache, marketplace, plugin, v));
1322
- const newest = versions.sort((a, b) => statSync2(b).mtimeMs - statSync2(a).mtimeMs)[0];
1427
+ const newest = versions.sort((a, b) => statSync3(b).mtimeMs - statSync3(a).mtimeMs)[0];
1323
1428
  return newest ? skillDirsIn(join11(newest, "skills")).map((path) => ({ name: basename4(path), plugin, path })) : [];
1324
1429
  })
1325
1430
  );
@@ -1388,7 +1493,7 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
1388
1493
  if (dep) {
1389
1494
  primary = reals.find((r) => rel(r.dir) === (dep.dir ?? PROJECT_SKILLS_DIR));
1390
1495
  if (!primary) continue;
1391
- } else if (chosen) {
1496
+ } else if (chosen && reals.some((r) => r.dir === chosen)) {
1392
1497
  primary = reals.find((r) => r.dir === chosen);
1393
1498
  } else {
1394
1499
  if (git2 === void 0) git2 = gitInfo(root);
@@ -1412,7 +1517,8 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
1412
1517
  if (!needed) steps.push({ kind: "unlink", path: e.path });
1413
1518
  } else steps.push(needed ? { kind: "link", path: e.path, target: primary.path } : { kind: "unlink", path: e.path });
1414
1519
  } else {
1415
- steps.push(needed ? { kind: "replace", path: e.path, target: primary.path } : { kind: "remove", path: e.path });
1520
+ const differs = e.hash !== primary.hash ? { differs: true } : {};
1521
+ steps.push(needed ? { kind: "replace", path: e.path, target: primary.path, ...differs } : { kind: "remove", path: e.path, ...differs });
1416
1522
  }
1417
1523
  }
1418
1524
  if (steps.length) report.plans.push({ name, root, primary: primary.path, steps });
@@ -1420,7 +1526,7 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
1420
1526
  return report;
1421
1527
  }
1422
1528
  function globalReadsOf(id) {
1423
- return HARNESSES.find((h) => h.id === id).globalDirs();
1529
+ return HARNESSES.find((h) => h.id === id).globalDirs().map(globalDirAs);
1424
1530
  }
1425
1531
  function isSkillsDirPlugin(path) {
1426
1532
  return existsSync11(join12(path, ".claude-plugin", "plugin.json"));
@@ -1435,15 +1541,16 @@ function planGlobalTidy({ keep = {}, enabled = enabledHarnesses() } = {}) {
1435
1541
  report.skipped.push({ name, reason: "a Claude Code plugin kept in a skills folder" });
1436
1542
  continue;
1437
1543
  }
1438
- const reals = entries.filter((e) => !e.link);
1544
+ const reals = entries.filter((e, i, all) => !e.link && all.findIndex((o) => !o.link && realpathOrNull2(o.path) === realpathOrNull2(e.path)) === i);
1439
1545
  if (reals.length < 2) continue;
1440
1546
  hashReals(reals);
1441
- const primary = reals.find((r) => r.dir === keep[name]) ?? reals.find((r) => r.dir === agents) ?? reals[0];
1442
- if (!keep[name] && reals.some((r) => r.hash !== primary.hash)) {
1547
+ const kept = reals.find((r) => r.dir === keep[name]);
1548
+ const primary = kept ?? reals.find((r) => r.dir === agents) ?? reals[0];
1549
+ if (!kept && reals.some((r) => r.hash !== primary.hash)) {
1443
1550
  report.conflicts.push(conflictOf(name, null, reals, enabled, globalReadsOf));
1444
1551
  continue;
1445
1552
  }
1446
- const steps = reals.filter((r) => r !== primary).map((r) => ({ kind: "replace", path: r.path, target: primary.path }));
1553
+ const steps = reals.filter((r) => r !== primary).map((r) => ({ kind: "replace", path: r.path, target: primary.path, ...r.hash !== primary.hash ? { differs: true } : {} }));
1447
1554
  report.plans.push({ name, root: null, primary: primary.path, steps });
1448
1555
  }
1449
1556
  return report;
@@ -1481,20 +1588,27 @@ function applyTidy(plan, { git: git2 = "keep", info: info2 } = {}) {
1481
1588
  return { name: plan.name, root: plan.root, applied, held };
1482
1589
  }
1483
1590
  function describeStep(step, root) {
1484
- const shown = root ? relativeTo(root, step.path) : step.path.replace(userHome(), "~");
1591
+ const shown = root ? relativeTo(root, step.path) : tildify(step.path);
1485
1592
  switch (step.kind) {
1486
1593
  case "link":
1487
1594
  return `link ${shown}`;
1488
1595
  case "unlink":
1489
1596
  return `remove extra link ${shown}`;
1490
1597
  case "replace":
1491
- return `${shown}: duplicate copy \u2192 link`;
1598
+ return `${shown}: ${step.differs ? "differing" : "duplicate"} copy \u2192 link`;
1492
1599
  case "remove":
1493
- return `remove duplicate copy ${shown}`;
1600
+ return `remove ${step.differs ? "differing" : "duplicate"} copy ${shown}`;
1494
1601
  }
1495
1602
  }
1496
1603
  function copyLabel(conflict, dir) {
1497
- return conflict.root ? relativeTo(conflict.root, dir) : dir.replace(userHome(), "~");
1604
+ return folderLabel(conflict.root, dir);
1605
+ }
1606
+ function folderLabel(root, dir) {
1607
+ return root ? relativeTo(root, dir) : tildify(dir);
1608
+ }
1609
+ function foldersOf(root, name) {
1610
+ const entries = entriesOf(root ? ALL_PROJECT_DIRS.map((d) => join12(root, d)) : globalSkillDirs(), name);
1611
+ return { all: entries.map((e) => e.dir), real: entries.filter((e) => !e.link).map((e) => e.dir) };
1498
1612
  }
1499
1613
 
1500
1614
  // src/health.ts
@@ -1543,13 +1657,14 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
1543
1657
  issues.push({
1544
1658
  id: `broken:${skill.name}`,
1545
1659
  severity: "problem",
1546
- title: `${skill.name}: broken link in ${skill.path.replace(/\/[^/]+$/, "").replace(/^\/Users\/[^/]+/, "~")}`,
1660
+ title: `${skill.name}: broken link in ${tildify(dirname5(skill.path))}`,
1547
1661
  detail: "It points at a folder that no longer exists, so it loads nothing.",
1548
1662
  fix: {
1549
1663
  label: "Remove the link",
1550
1664
  run: () => {
1551
1665
  const r = unloadGlobal(skill.path);
1552
- return r.ok ? `Removed broken link ${skill.name}` : `${skill.name}: ${r.reason}`;
1666
+ if (!r.ok) throw new Error(`${skill.name}: ${r.reason}`);
1667
+ return `Removed broken link ${skill.name}`;
1553
1668
  }
1554
1669
  }
1555
1670
  });
@@ -1570,8 +1685,10 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
1570
1685
  });
1571
1686
  }
1572
1687
  const loadedHere = new Map(projects2.map((root) => [root, loadedIn(root)]));
1573
- const seenByClaude = (s) => s.visibility.some((v) => v.id === "claude-code" && v.paths > 0);
1574
- const namesIn2 = (root) => [...new Set([...statuses.get(root), ...nested.get(root)].filter(seenByClaude).map((s) => s.name))];
1688
+ const namesIn2 = (root, agent = "claude-code") => [
1689
+ ...new Set([...statuses.get(root), ...nested.get(root)].filter((s) => s.visibility.some((v) => v.id === agent && v.paths > 0)).map((s) => s.name))
1690
+ ];
1691
+ const pluginAgentsIn = (plugin, root) => new Set(loadedHere.get(root).filter((m) => plugin.skills.includes(m.path)).flatMap((m) => m.harnesses));
1575
1692
  const globalYours = new Set(loaded.filter((m) => m.movable).map((m) => m.name));
1576
1693
  const installedYours = /* @__PURE__ */ new Set([...globalYours, ...[...statuses.values()].flat().map((s) => s.name)]);
1577
1694
  const repoChoices = (plugin, root, names) => {
@@ -1579,7 +1696,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
1579
1696
  const copies = statuses.get(root).filter((s) => names.includes(s.name));
1580
1697
  const allManaged = names.every((n) => copies.some((c) => c.name === n && c.managed));
1581
1698
  return [
1582
- { label: `Turn the plugin ${plugin.id} off in ${where} only`, hint: "other repos keep it", run: () => orThrow(turnOffIn(plugin, root)) },
1699
+ { label: `Turn the plugin ${plugin.id} off in ${where} only`, hint: "other repos keep it; in .claude/settings.local.json, a local-only file", run: () => orThrow(turnOffIn(plugin, root)) },
1583
1700
  // Only copies skilllib installed: Your skills keeps them. A repo's own skill is the team's, never removed from here.
1584
1701
  ...names.length && allManaged ? [
1585
1702
  {
@@ -1599,8 +1716,10 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
1599
1716
  const machinePlugins = claudePlugins(machine);
1600
1717
  for (const plugin of machinePlugins) {
1601
1718
  const names = plugin.skills.map((p) => basename6(p));
1602
- const inRepos = projects2.filter((root) => loadedHere.get(root).some((m) => plugin.skills.includes(m.path))).map((root) => [root, namesIn2(root).filter((n) => names.includes(n))]).filter(([, d]) => d.length);
1603
- const loadedYours = /* @__PURE__ */ new Set([...globalYours, ...inRepos.flatMap(([, d]) => d)]);
1719
+ 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);
1720
+ 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) : [];
1721
+ const claudeYours = /* @__PURE__ */ new Set([...globalYours, ...inRepos.flatMap(([, d]) => d)]);
1722
+ const loadedYours = /* @__PURE__ */ new Set([...claudeYours, ...cursorRepos.flatMap(([, d]) => d)]);
1604
1723
  const dupes = names.filter((n) => libraryNames.has(n) || loadedYours.has(n));
1605
1724
  if (!dupes.length) continue;
1606
1725
  const others = plugin.skills.length - dupes.length;
@@ -1612,15 +1731,18 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
1612
1731
  };
1613
1732
  const off = { label: `Turn the plugin ${plugin.id} off`, hint: "stays installed", run: () => orThrow(removePlugin(plugin, "off")) };
1614
1733
  const bothLoad = dupes.filter((n) => loadedYours.has(n));
1734
+ const claudeBoth = dupes.filter((n) => claudeYours.has(n));
1735
+ const cursorWheres = cursorRepos.map(([root]) => basename6(root));
1615
1736
  const notInstalled = dupes.filter((n) => !installedYours.has(n));
1616
1737
  issues.push({
1617
1738
  id: `plugin:${plugin.id}`,
1618
1739
  severity: bothLoad.length ? "problem" : "suggestion",
1619
1740
  title: `${dupes.join(", ")}: also in the Claude Code plugin ${plugin.id}`,
1620
1741
  detail: [
1621
- bothLoad.length ? `Claude Code loads both (the plugin's as /${pluginName}:${bothLoad[0]}). Your skill wins.` : "",
1742
+ claudeBoth.length ? `Claude Code loads both (the plugin's as /${pluginName}:${claudeBoth[0]}). Your skill wins.` : "",
1743
+ cursorWheres.length ? `${cursorWheres.join(", ")} ${cursorWheres.length === 1 ? "turns" : "turn"} it off for Claude Code, but Cursor ignores repo settings and lists both there: only turning the plugin off or removing it changes that.` : "",
1622
1744
  notInstalled.length ? `Your ${notInstalled.join(", ")} ${notInstalled.length === 1 ? "is" : "are"} only in Your skills, not installed anywhere: once the plugin is gone, add ${notInstalled.length === 1 ? "it" : "them"} where you need ${notInstalled.length === 1 ? "it" : "them"}.` : "",
1623
- others ? `The plugin's other ${plural(others, "skill")} are copied into Your skills first, so nothing is lost.` : "",
1745
+ others ? `The plugin's other ${others === 1 ? "skill is" : `${others} skills are`} copied into Your skills first, so nothing is lost.` : "",
1624
1746
  plugin.extras.length ? `It also brings ${plugin.extras.join(", ")}: ${plugin.synced ? "turning it off pauses those" : "removing it drops those, turning it off pauses them"}.` : "",
1625
1747
  plugin.synced ? "It's synced from claude.ai, so it can only be turned off here." : "",
1626
1748
  inRepos.length ? `Or keep it and turn it off only in ${inRepos.map(([root]) => basename6(root)).join(", ")}.` : ""
@@ -1667,7 +1789,11 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
1667
1789
  detail: `${global.plans.map((p) => p.name).join(", ")}: each keeps one copy (in ~/.agents/skills when there's one) and the others become links. Restorable from Health.`,
1668
1790
  fix: {
1669
1791
  label: "Keep one copy of each",
1670
- run: () => `${plural(applyAll(global.plans, "go").skills, "global skill")} now have one copy`
1792
+ run: () => {
1793
+ const r = applyAll(global.plans, "go");
1794
+ if (!r.skills) throw new Error("No global skill changed");
1795
+ return `${plural(r.skills, "global skill")} now ${r.skills === 1 ? "has" : "have"} one copy`;
1796
+ }
1671
1797
  }
1672
1798
  });
1673
1799
  }
@@ -1679,7 +1805,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
1679
1805
  id: "review:global",
1680
1806
  severity: "suggestion",
1681
1807
  title: `${plural(unreviewed.length, "global skill")} of yours ${unreviewed.length === 1 ? "loads" : "load"} in every repo`,
1682
- detail: `${unreviewed.join(", ")}. Choose which repos keep each (Global \u2192 Clean up\u2026), or mark the ones you want everywhere as global on purpose (skilllib global keep <name>).`
1808
+ detail: `${unreviewed.join(", ")}. Move each to the repos that need it (skilllib \u2192 Global \u2192 Move it to the repos that need it\u2026), or mark the ones you want everywhere as global on purpose (skilllib global keep <name>).`
1683
1809
  });
1684
1810
  }
1685
1811
  const enabled = enabledHarnesses();
@@ -1697,6 +1823,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
1697
1823
  run: () => {
1698
1824
  const results = skills.map((m) => ({ m, ...linkGlobal(m.path, blindOf(m)) }));
1699
1825
  const skipped = results.flatMap((r) => r.skipped);
1826
+ if (!results.some((r) => r.linked.length)) throw new Error(`Nothing linked${skipped.length ? `; skipped ${skipped.join(", ")} (a different skill has that name)` : ""}`);
1700
1827
  return `Linked ${plural(results.filter((r) => r.linked.length).length, "global skill")} for ${agentNames(missing)}${skipped.length ? `; skipped ${skipped.join(", ")} (a different skill has that name)` : ""}`;
1701
1828
  }
1702
1829
  };
@@ -1714,28 +1841,69 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
1714
1841
  for (const root of projects2) {
1715
1842
  const where = basename6(root);
1716
1843
  const status2 = statuses.get(root);
1717
- const behind = status2.filter((s) => s.state === "update available" || s.state === "folder missing");
1718
- if (behind.length > 0) {
1844
+ const restorable = status2.filter((s) => s.state === "folder missing" && s.latest !== null);
1845
+ if (restorable.length > 0) {
1719
1846
  issues.push({
1720
1847
  id: `sync:${root}`,
1721
1848
  severity: "problem",
1722
- title: `${where}: ${plural(behind.length, "skill")} out of date`,
1723
- detail: behind.map((s) => s.name).join(", "),
1849
+ title: `${where}: ${plural(restorable.length, "skill")} in skilllib.json not installed`,
1850
+ detail: `${restorable.map((s) => s.name).join(", ")}: the folder is missing`,
1724
1851
  fix: {
1725
1852
  label: "Sync the project",
1726
1853
  run: () => {
1727
1854
  const changes = syncProject(root);
1728
- return `${where}: ${changes.filter((c) => c.action !== "skipped").length} updated`;
1855
+ const done = changes.filter((c) => c.action !== "skipped");
1856
+ const skipped = changes.filter((c) => c.action === "skipped").map((c) => `${c.name}: ${c.reason}`);
1857
+ if (!done.length) throw new Error(`${where}: nothing restored${skipped.length ? `; ${skipped.join("; ")}` : ""}`);
1858
+ return `${where}: restored ${plural(done.length, "skill")}${skipped.length ? `; skipped ${skipped.join("; ")}` : ""}`;
1729
1859
  }
1730
1860
  }
1731
1861
  });
1732
1862
  }
1863
+ for (const s of status2.filter((s2) => s2.state === "folder missing" && s2.latest === null))
1864
+ issues.push({
1865
+ id: `unavailable:${root}:${s.name}`,
1866
+ severity: "problem",
1867
+ title: `${s.name}: pinned in ${where}'s skilllib.json, but not in your library`,
1868
+ detail: `Its folder is missing, and your library (${tildify(libraryDir())}) has no copy to restore: import it, or set SKILLLIB_HOME to the library that has it.`,
1869
+ // Editing skilllib.json is the team's call: a choice, never run by `doctor --fix`.
1870
+ choices: [
1871
+ {
1872
+ label: "Remove it from skilllib.json",
1873
+ hint: `${where} stops pinning it`,
1874
+ run: () => {
1875
+ const r = removeSkill(root, s.name);
1876
+ if (r.action !== "removed") throw new Error(`${s.name}: ${r.reason}`);
1877
+ return `${s.name}: removed from ${where}'s skilllib.json`;
1878
+ }
1879
+ }
1880
+ ]
1881
+ });
1882
+ const behind = status2.filter((s) => s.state === "update available");
1883
+ if (behind.length > 0)
1884
+ issues.push({
1885
+ id: `update:${root}`,
1886
+ severity: "suggestion",
1887
+ title: `${where}: ${plural(behind.length, "skill")} out of date`,
1888
+ detail: behind.map((s) => `${s.name} (v${s.version} \u2192 v${s.latest})`).join(", "),
1889
+ fix: {
1890
+ label: "Update to the newest versions",
1891
+ run: () => {
1892
+ const changes = updateProject(root, behind.map((s) => s.name));
1893
+ const done = changes.filter((c) => c.action !== "skipped");
1894
+ const skipped = changes.filter((c) => c.action === "skipped").map((c) => `${c.name}: ${c.reason}`);
1895
+ if (!done.length) throw new Error(`${where}: nothing updated${skipped.length ? `; ${skipped.join("; ")}` : ""}`);
1896
+ return `${where}: updated ${plural(done.length, "skill")}${skipped.length ? `; skipped ${skipped.join("; ")}` : ""}`;
1897
+ }
1898
+ }
1899
+ });
1733
1900
  const tidy2 = planProjectTidy(root);
1734
1901
  if (tidy2.plans.length) {
1735
- const git2 = gitInfo(root);
1736
- const visible2 = tidy2.plans.flatMap((p) => gitVisibleSteps(p, git2).map((s) => describeStep(s, root)));
1902
+ const git3 = gitInfo(root);
1903
+ const visible2 = tidy2.plans.flatMap((p) => gitVisibleSteps(p, git3).map((s) => describeStep(s, root)));
1737
1904
  const run = (mode) => () => {
1738
1905
  const r = applyAll(tidy2.plans, mode);
1906
+ if (!r.skills) throw new Error(`${where}: nothing tidied${r.held ? `; left ${plural(r.held, "change")} git would see` : ""}`);
1739
1907
  return `${where}: tidied ${plural(r.skills, "skill")}${r.held ? `; left ${plural(r.held, "change")} git would see` : ""}`;
1740
1908
  };
1741
1909
  issues.push({
@@ -1752,14 +1920,24 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
1752
1920
  });
1753
1921
  }
1754
1922
  issues.push(...tidy2.conflicts.map(conflictIssue));
1755
- const partial = status2.filter((s) => s.state !== "folder missing" && s.visibility.some((v) => v.paths === 0));
1923
+ const blindHere = status2.filter((s) => s.state !== "folder missing" && s.visibility.some((v) => v.paths === 0)).map((s) => ({ s, would: linkEverywhere(root, s.name, s.location, { dryRun: true }) }));
1924
+ for (const { s, would } of blindHere.filter(({ would: w }) => !w.created.length && !w.blocked.length && w.uncommitted.length))
1925
+ issues.push({
1926
+ id: `uncommitted:${root}:${s.name}`,
1927
+ severity: "suggestion",
1928
+ 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`,
1929
+ 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\`, or from Health).`
1930
+ });
1931
+ const partial = blindHere.filter(({ would }) => would.created.length || would.blocked.length).map(({ s }) => s);
1756
1932
  if (partial.length) {
1757
1933
  const missing = [...new Set(partial.flatMap((s) => s.visibility.filter((v) => v.paths === 0).map((v) => v.id)))];
1758
- const tracked = readConfig().agentsDirOk?.includes(root) ? [] : [...new Set(missing.map((id) => harness(id).projectDirs[0]))].filter((dir) => isGitTracked(root, dir));
1934
+ const tracked = [...new Set(blindHere.flatMap(({ would }) => would.blocked))];
1759
1935
  const run = (allowTracked) => () => {
1760
1936
  if (allowTracked) allowTrackedLinks(root);
1761
1937
  const r = linkAll(root, { allowTracked });
1762
- return `${where}: linked ${plural(r.linked.length, "skill")} for ${agentNames(missing)}${r.blocked.length ? `; skipped ${r.blocked.join(", ")} (git tracks it)` : ""}`;
1938
+ 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)` : ""}`;
1939
+ if (!r.linked.length) throw new Error(`${where}: nothing linked${notes}`);
1940
+ return `${where}: linked ${plural(r.linked.length, "skill")} for ${agentNames(missing)}${notes}`;
1763
1941
  };
1764
1942
  issues.push({
1765
1943
  id: `usable:${root}`,
@@ -1768,20 +1946,40 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
1768
1946
  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.` : ""}`,
1769
1947
  ...tracked.length ? {
1770
1948
  choices: [
1771
- { label: "Add links, but not where git tracks the folder", hint: `skips ${tracked.join(", ")}`, run: run(false) },
1949
+ // Only when some link goes where git doesn't track: otherwise this one would link nothing.
1950
+ ...blindHere.some(({ would }) => would.created.length) ? [{ label: "Add links, but not where git tracks the folder", hint: `skips ${tracked.join(", ")}`, run: run(false) }] : [],
1772
1951
  { label: "Add links everywhere", hint: `remembered for ${where}; git will see them`, run: run(true) }
1773
1952
  ]
1774
1953
  } : { fix: { label: "Add the links", run: run(false) } }
1775
1954
  });
1776
1955
  }
1777
1956
  const differing = new Set(tidy2.conflicts.map((c) => c.name));
1957
+ const git2 = gitInfo(root);
1958
+ const committed = (s) => ["committed", "changed"].includes(git2?.of(relativeTo(root, s.path)) ?? "");
1778
1959
  for (const s of status2) {
1779
1960
  if (isLink(s.path) || differing.has(s.name)) continue;
1780
- if (s.state === "local only") {
1961
+ if (s.state === "local only" && committed(s)) {
1962
+ issues.push({
1963
+ id: `local:${root}:${s.name}`,
1964
+ severity: "suggestion",
1965
+ title: `${s.name} in ${where} isn't in your library`,
1966
+ detail: "Copy it into the library to reuse it in other projects. The repo's copy stays as it is: it's the team's (committed).",
1967
+ choices: [
1968
+ {
1969
+ label: "Copy into the library",
1970
+ hint: `${where}'s copy stays as it is`,
1971
+ run: () => {
1972
+ importSkill(s.path);
1973
+ return `${s.name} copied into your library; ${where}'s copy stays as it is`;
1974
+ }
1975
+ }
1976
+ ]
1977
+ });
1978
+ } else if (s.state === "local only") {
1781
1979
  issues.push({
1782
1980
  id: `local:${root}:${s.name}`,
1783
1981
  severity: "suggestion",
1784
- title: `${s.name}: only exists in ${where}`,
1982
+ title: `${s.name} in ${where} isn't in your library`,
1785
1983
  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.",
1786
1984
  choices: [
1787
1985
  {
@@ -1984,7 +2182,7 @@ function groupBy(items) {
1984
2182
  }
1985
2183
  var sameSet = (a, b) => a.length === b.length && a.every((x) => b.includes(x));
1986
2184
  function globalFrom(kind, origin, path) {
1987
- if (kind === "global") return tildify(path.replace(/[\\/][^\\/]+$/, "")).replace(/\\/g, "/");
2185
+ if (kind === "global" || kind === "skilllib") return tildify(path.replace(/[\\/][^\\/]+$/, "")).replace(/\\/g, "/");
1988
2186
  return origin;
1989
2187
  }
1990
2188
  var SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "target", "vendor", "coverage"]);
@@ -1997,28 +2195,34 @@ function topLevel(root) {
1997
2195
  }
1998
2196
  function usableHere(root, uses = /* @__PURE__ */ new Map()) {
1999
2197
  const agents = enabledHarnesses();
2000
- const git2 = gitInfo(root);
2001
- const inRepo = isProjectCandidate(root) && (existsSync12(join13(root, ".git")) || existsSync12(join13(root, MANIFEST_FILE)));
2002
- const project = inRepo ? projectStatus(root) : [];
2003
- const skills = groupBy(
2004
- project.map((s) => {
2198
+ const inRepo = projectHere(root) === root;
2199
+ const nestedHere = inRepo ? nestedSkills(root, { enabled: agents }) : [];
2200
+ const git2 = gitInfo(root, [...new Set(nestedHere.map((s) => s.location))]);
2201
+ const group = (list2) => groupBy(
2202
+ list2.map((s) => {
2005
2203
  const loadedBy = s.visibility.filter((v) => v.paths > 0).map((v) => v.id);
2006
2204
  const behind = s.managed && s.latest !== null && s.version !== null && s.latest > s.version;
2205
+ const gitState = git2?.of(relativeTo(root, s.path));
2206
+ const repo = !s.managed && (s.state.startsWith("repo skill") || gitState === "committed" || gitState === "changed");
2207
+ const usual = s.state === "ok" || s.state === "local only" || repo && s.state !== "from npx skills";
2007
2208
  const attrs = {
2008
- source: s.managed ? "library" : s.state.startsWith("repo skill") ? "repo" : "local",
2209
+ source: s.managed ? "library" : repo ? "repo" : "local",
2009
2210
  ...s.location !== ".claude/skills" && { dir: s.location },
2010
2211
  ...s.version !== null && { version: s.version },
2011
- ...s.state !== "ok" && s.state !== "local only" && !s.state.startsWith("repo skill") && { state: s.state },
2212
+ ...!usual && { state: s.state },
2012
2213
  ...behind && { latest: s.latest },
2013
- ...git2 && { git: git2.of(relativeTo(root, s.path)) },
2214
+ ...gitState && { git: gitState },
2014
2215
  ...!sameSet(loadedBy, agents) && { agents: loadedBy }
2015
2216
  };
2016
2217
  return [JSON.stringify(attrs), s.name, () => ({ ...attrs, skills: [] })];
2017
2218
  })
2018
2219
  );
2220
+ const skills = group(inRepo ? projectStatus(root) : []);
2221
+ const nested = group(nestedHere);
2019
2222
  const machine = machineSkills(agents);
2223
+ const loaded = inRepo ? skillsLoadedIn(root, machine, agents) : machine;
2020
2224
  const global = groupBy(
2021
- machine.filter((m) => !m.broken && m.harnesses.length > 0).map((m) => {
2225
+ loaded.filter((m) => !m.broken && m.harnesses.length > 0).map((m) => {
2022
2226
  const from = globalFrom(m.kind, m.origin, m.path);
2023
2227
  return [`${m.kind}\0${from}\0${m.harnesses.join()}`, m.name, () => ({ source: m.kind, from, agents: m.harnesses, skills: [] })];
2024
2228
  })
@@ -2037,6 +2241,7 @@ function usableHere(root, uses = /* @__PURE__ */ new Map()) {
2037
2241
  files: topLevel(root),
2038
2242
  agents,
2039
2243
  skills,
2244
+ ...nested.length > 0 && { nested },
2040
2245
  global,
2041
2246
  issues: [...issues.values()],
2042
2247
  ...Object.keys(usesHere).length > 0 && { usesHere }
@@ -2047,9 +2252,9 @@ function firstSentence(description) {
2047
2252
  const sentence = line.match(/^(.{40,}?[.!?])(?:\s|$)/)?.[1] ?? line;
2048
2253
  return sentence.length > 200 ? sentence.slice(0, 199).trimEnd() + "\u2026" : sentence;
2049
2254
  }
2050
- function libraryFor(currentProject2) {
2255
+ function libraryFor(currentProject) {
2051
2256
  const manifests = visibleProjects().map((p) => ({ name: basename7(p), skills: readManifest(p).skills }));
2052
- const here = currentProject2 ? new Set(projectStatus(currentProject2).map((s) => s.name)) : null;
2257
+ const here = currentProject ? new Set(projectStatus(currentProject).map((s) => s.name)) : null;
2053
2258
  const all = librarySkills();
2054
2259
  const describe = (s) => {
2055
2260
  const projects2 = manifests.filter((m) => s.name in m.skills).map((m) => m.name);
@@ -2060,8 +2265,8 @@ function libraryFor(currentProject2) {
2060
2265
  }
2061
2266
 
2062
2267
  // src/usage.ts
2063
- import { createReadStream, existsSync as existsSync13, readdirSync as readdirSync8, statSync as statSync3 } from "fs";
2064
- import { join as join14 } from "path";
2268
+ import { createReadStream, existsSync as existsSync12, readdirSync as readdirSync8, statSync as statSync4 } from "fs";
2269
+ import { join as join13 } from "path";
2065
2270
  import { createInterface } from "readline";
2066
2271
  var BUILTIN_COMMANDS = /* @__PURE__ */ new Set([
2067
2272
  "add-dir",
@@ -2141,12 +2346,12 @@ function usesFromTranscriptLine(line) {
2141
2346
  return [];
2142
2347
  }
2143
2348
  function transcriptFiles(sinceMs) {
2144
- const base = join14(claudeDir(), "projects");
2145
- if (!existsSync13(base)) return [];
2349
+ const base = join13(claudeDir(), "projects");
2350
+ if (!existsSync12(base)) return [];
2146
2351
  return readdirSync8(base).flatMap((project) => {
2147
- const dir = join14(base, project);
2148
- if (!statSync3(dir).isDirectory()) return [];
2149
- return readdirSync8(dir).filter((f) => f.endsWith(".jsonl")).map((f) => join14(dir, f)).filter((f) => statSync3(f).mtimeMs >= sinceMs);
2352
+ const dir = join13(base, project);
2353
+ if (!statSync4(dir).isDirectory()) return [];
2354
+ return readdirSync8(dir).filter((f) => f.endsWith(".jsonl")).map((f) => join13(dir, f)).filter((f) => statSync4(f).mtimeMs >= sinceMs);
2150
2355
  });
2151
2356
  }
2152
2357
  async function scanUsage(days) {
@@ -2210,29 +2415,40 @@ function parseArgs(argv) {
2210
2415
  function colorState(state) {
2211
2416
  if (state === "ok") return green(state);
2212
2417
  if (state === "folder missing" || state === "not in library") return red(state);
2213
- if (state === "local only" || state === "from npx skills") return dim(state);
2418
+ if (state === "local only" || state === "from npx skills" || state === "repo skill") return dim(state);
2214
2419
  return yellow(state);
2215
2420
  }
2216
2421
  function withDirs(root, changes) {
2217
2422
  const { skills } = readManifest(root);
2218
2423
  return changes.map((c) => {
2219
- const dep = c.action === "installed" || c.action === "updated" ? skills[c.name] : void 0;
2424
+ const dep = c.action === "installed" || c.action === "updated" || c.action === "reset" ? skills[c.name] : void 0;
2220
2425
  return dep ? { ...c, dirs: [dep.dir ?? ".claude/skills", ...dep.links ?? []] } : c;
2221
2426
  });
2222
2427
  }
2223
2428
  function printChanges(root, changes, args) {
2429
+ if (changes.some((c) => c.action === "skipped")) process.exitCode = 1;
2224
2430
  if (args.json) return json(withDirs(root, changes));
2225
2431
  for (const c of changes) {
2226
2432
  if (c.blocked?.length) {
2227
2433
  warn(`${c.name}: git tracks ${c.blocked.join(", ")} here, so no link was added there. Re-run with --allow-tracked to add it.`);
2228
2434
  }
2229
- if (c.action === "skipped") warn(`${c.name}: ${c.reason}`);
2435
+ if (c.uncommitted?.length) {
2436
+ warn(
2437
+ `${c.name}: git tracks ${c.uncommitted.join(", ")} here but not ${c.name}'s real copy, so no link was added there (teammates would get a broken link). Commit ${c.name} first, then run skilllib link --allow-tracked.`
2438
+ );
2439
+ }
2440
+ if (c.action === "skipped") error(`${c.name}: ${c.reason}`);
2230
2441
  else {
2231
- const icon = c.action === "removed" ? red("-") : green(c.action === "installed" ? "+" : "\u2191");
2442
+ const icon = c.action === "removed" ? red("-") : c.action === "reset" ? yellow("\u21BA") : green(c.action === "installed" ? "+" : "\u2191");
2232
2443
  const versions = c.action === "removed" ? "" : c.from && c.from !== c.to ? ` v${c.from} \u2192 v${c.to}` : c.to ? ` v${c.to}` : "";
2233
2444
  info(` ${icon} ${c.name}${versions} ${dim(c.action)}`);
2234
2445
  }
2446
+ if (c.backedUp) info(dim(` local edits saved to ${tildify(c.backedUp)}; \`skilllib restore ${c.name}\` brings them back`));
2235
2447
  }
2448
+ const done = changes.filter((c) => c.action !== "skipped").length;
2449
+ const skipped = changes.length - done;
2450
+ if (skipped && done) info(dim(`
2451
+ ${done} done, ${skipped} skipped`));
2236
2452
  }
2237
2453
  function projectOfFactory(projects2) {
2238
2454
  const sorted = [...projects2].sort((a, b) => b.length - a.length);
@@ -2250,35 +2466,51 @@ async function usageBySkill(days, onlyProject) {
2250
2466
  const scoped = onlyProject ? uses.filter((u) => projectOf(u.cwd) === onlyProject) : uses;
2251
2467
  return new Map(summarize(scoped, projectOf).map((s) => [s.skill, s]));
2252
2468
  }
2253
- function currentProject() {
2254
- const root = findProjectRoot();
2255
- return isProjectCandidate(root) && (existsSync14(join15(root, ".git")) || existsSync14(join15(root, "skilllib.json"))) ? root : null;
2469
+ function requireProject() {
2470
+ const root = projectHere();
2471
+ if (root) return root;
2472
+ if (!isProjectCandidate(findProjectRoot())) {
2473
+ const stray = existsSync13(join14(userHome(), MANIFEST_FILE)) ? `, though ${tildify(join14(userHome(), MANIFEST_FILE))} makes it look like one` : "";
2474
+ throw new Error(`Your home folder isn't a project${stray}: skills added here would load in every repo. Run this inside a project.`);
2475
+ }
2476
+ throw new Error("Not in a git repo. Run this inside a project (or `git init` first).");
2256
2477
  }
2257
2478
  function lastUsed(summary) {
2258
2479
  return summary ? summary.lastUsed.slice(0, 10) : "-";
2259
2480
  }
2260
2481
  async function status(args) {
2261
- const root = findProjectRoot();
2262
2482
  if (args.json) {
2263
- const usage3 = await usageBySkill(args.days, root);
2264
- json(usableHere(root, new Map([...usage3].map(([name, u]) => [name, u.uses]))));
2265
- if (currentProject()) rememberProjects([root]);
2483
+ const root2 = findProjectRoot();
2484
+ const usage3 = await usageBySkill(args.days, root2);
2485
+ json(usableHere(root2, new Map([...usage3].map(([name, u]) => [name, u.uses]))));
2486
+ if (projectHere()) rememberProjects([root2]);
2266
2487
  return;
2267
2488
  }
2489
+ const root = requireProject();
2268
2490
  const skills = projectStatus(root);
2269
2491
  const nested = nestedSkills(root);
2270
2492
  info(`${basename8(root)} ${dim(tildify(root))}
2271
2493
  `);
2272
- if (skills.length === 0 && nested.length === 0) {
2273
- info("No skills in this project yet. Add one from your library with: skilllib add <name>");
2274
- return;
2275
- }
2494
+ if (skills.length === 0 && nested.length === 0) info("No skills in this project yet. Add one from your library with: skilllib add <name>");
2276
2495
  const usage2 = await usageBySkill(args.days, root);
2496
+ const git2 = gitInfo(root, [...new Set(nested.map((s) => s.location))]);
2497
+ const committed = (path) => ["committed", "changed"].includes(git2?.of(relativeTo(root, path)) ?? "");
2498
+ const asRepo = {
2499
+ "local only": "repo skill",
2500
+ "untracked copy of library skill": "repo skill, in library",
2501
+ "untracked, differs from library": "repo skill, differs from library"
2502
+ };
2503
+ const stateOf = (s) => colorState(committed(s.path) && asRepo[s.state] || s.state);
2504
+ const agentsOf = (s) => {
2505
+ const ids = s.visibility.filter((v) => v.paths > 0).map((v) => v.id);
2506
+ return ids.length === s.visibility.length ? dim("all") : ids.length ? ids.map((id) => harness(id).name).join(", ") : red("none");
2507
+ };
2277
2508
  if (skills.length) {
2278
2509
  table(
2279
2510
  skills.map((s) => ({
2280
2511
  Skill: s.name,
2281
- Status: s.source ? dim(`npx skills: ${s.source}`) : colorState(s.state),
2512
+ Status: s.source ? dim(`npx skills: ${s.source}`) : stateOf(s),
2513
+ Agents: agentsOf(s),
2282
2514
  [`Uses (${args.days}d)`]: String(usage2.get(s.name)?.uses ?? 0),
2283
2515
  "Last used": lastUsed(usage2.get(s.name))
2284
2516
  }))
@@ -2290,7 +2522,8 @@ async function status(args) {
2290
2522
  nested.map((s) => ({
2291
2523
  Skill: s.name,
2292
2524
  Folder: s.location,
2293
- Status: colorState(s.state),
2525
+ Status: stateOf(s),
2526
+ Agents: agentsOf(s),
2294
2527
  [`Uses (${args.days}d)`]: String(usage2.get(s.name)?.uses ?? 0)
2295
2528
  }))
2296
2529
  );
@@ -2298,18 +2531,32 @@ async function status(args) {
2298
2531
  const states = new Set(skills.map((s) => s.state));
2299
2532
  const hints = [];
2300
2533
  if (states.has("update available")) hints.push("`skilllib update` to install the newest versions");
2301
- if (states.has("folder missing")) hints.push("`skilllib sync` to restore missing skills");
2534
+ const missing = skills.filter((s) => s.state === "folder missing");
2535
+ if (missing.some((s) => s.latest !== null)) hints.push("`skilllib sync` to restore missing skills");
2536
+ const notInLibrary = missing.filter((s) => s.latest === null).map((s) => s.name);
2537
+ if (notInLibrary.length)
2538
+ hints.push(`${notInLibrary.join(", ")} ${notInLibrary.length === 1 ? "isn't" : "aren't"} in your library (${tildify(libraryDir())}): \`skilllib import <dir>\` to add, or set SKILLLIB_HOME to the library that has ${notInLibrary.length === 1 ? "it" : "them"}`);
2302
2539
  if (states.has("edited locally") || states.has("edited locally, update available"))
2303
2540
  hints.push("`skilllib import .claude/skills/<name> --force` to save local edits to the library");
2304
2541
  if (states.has("local only")) hints.push("`skilllib import .claude/skills/<name>` to add a local skill to the library");
2305
2542
  if (states.has("untracked copy of library skill") || states.has("untracked, differs from library"))
2306
2543
  hints.push("`skilllib add <name>` to manage an untracked skill (--force if it differs)");
2307
2544
  if (hints.length > 0) info("\n" + hints.map((h) => dim(`\u2192 ${h}`)).join("\n"));
2545
+ const here = usableHere(root);
2546
+ const globals = new Set(here.global.flatMap((g) => g.skills)).size;
2547
+ if (globals) info(`
2548
+ ${globals} global ${globals === 1 ? "skill also loads" : "skills also load"} here ${dim("(your global folders, plugins, claude.ai): `skilllib` lists them")}`);
2549
+ const problems = here.issues.reduce((n, i) => n + i.problems.length, 0);
2550
+ if (problems) warn(`${problems} ${problems === 1 ? "issue" : "issues"} here or in your global skills: \`skilllib doctor\` lists them with fixes`);
2308
2551
  rememberProjects([root]);
2309
2552
  }
2553
+ async function overview(args) {
2554
+ if (args.json || projectHere()) return status(args);
2555
+ info("Not in a project. `skilllib projects` lists yours, `skilllib list` your library, `skilllib global` your global skills.");
2556
+ }
2310
2557
  async function list(args) {
2311
2558
  if (args.json) {
2312
- return json(libraryFor(currentProject()));
2559
+ return json(libraryFor(projectHere()));
2313
2560
  }
2314
2561
  const skills = librarySkills();
2315
2562
  if (skills.length === 0) {
@@ -2337,14 +2584,14 @@ async function list(args) {
2337
2584
  );
2338
2585
  }
2339
2586
  const libraryNames = new Set(skills.map((s) => s.name));
2340
- const globals = machineSkills().filter((m) => m.movable && !m.broken);
2587
+ const globals = [...new Set(machineSkills().filter((m) => m.movable && !m.broken).map((m) => m.name))];
2341
2588
  if (globals.length > 0) {
2342
2589
  info("");
2343
- warn(`${globals.length} of your global skill(s) still load in every project`);
2344
- const missing = globals.filter((g) => !libraryNames.has(g.name));
2590
+ warn(`${globals.length} of your global ${globals.length === 1 ? "skill still loads" : "skills still load"} in every project`);
2591
+ const missing = globals.filter((g) => !libraryNames.has(g));
2345
2592
  info(
2346
2593
  dim(
2347
- missing.length > 0 ? `\u2192 \`skilllib import --global\` copies ${missing.length} of them into the library` : "\u2192 all are in your library; run `skilllib` \u2192 Global skills to stop loading them everywhere"
2594
+ missing.length > 0 ? `\u2192 \`skilllib import --global\` copies ${missing.length} of them into the library` : "\u2192 all are in your library; run `skilllib` \u2192 Global to stop loading them everywhere"
2348
2595
  )
2349
2596
  );
2350
2597
  }
@@ -2384,7 +2631,7 @@ function add(args) {
2384
2631
  error("Usage: skilllib add <skill-name>... [--force]");
2385
2632
  process.exit(1);
2386
2633
  }
2387
- const root = findProjectRoot();
2634
+ const root = requireProject();
2388
2635
  if (args.allowTracked) allowTrackedLinks(root);
2389
2636
  printChanges(root, args.positional.map((name) => addSkill(root, name, { force: args.force })), args);
2390
2637
  rememberProjects([root]);
@@ -2394,18 +2641,21 @@ function remove(args) {
2394
2641
  error("Usage: skilllib remove <skill-name>... [--force]");
2395
2642
  process.exit(1);
2396
2643
  }
2397
- const root = findProjectRoot();
2644
+ const root = requireProject();
2398
2645
  printChanges(root, args.positional.map((name) => removeSkill(root, name, { force: args.force })), args);
2399
2646
  }
2400
2647
  function sync(args) {
2401
- const roots = args.all ? visibleProjects() : [findProjectRoot()];
2648
+ const roots = args.all ? visibleProjects() : [requireProject()];
2402
2649
  const results = roots.map((root) => {
2403
2650
  if (args.allowTracked) allowTrackedLinks(root);
2404
2651
  const changes = syncProject(root, { force: args.force });
2652
+ if (changes.some((c) => c.action === "skipped")) process.exitCode = 1;
2405
2653
  if (!args.json) {
2406
2654
  if (args.all) info(`${basename8(root)} ${dim(tildify(root))}`);
2407
- if (changes.length === 0) success("Installed versions match skilllib.json");
2408
- else printChanges(root, changes, args);
2655
+ if (changes.length === 0) {
2656
+ if (existsSync13(join14(root, MANIFEST_FILE))) success("Installed versions match skilllib.json");
2657
+ else info(dim("No skilllib.json: nothing to sync"));
2658
+ } else printChanges(root, changes, args);
2409
2659
  }
2410
2660
  return { project: basename8(root), path: root, changes: withDirs(root, changes) };
2411
2661
  });
@@ -2413,10 +2663,16 @@ function sync(args) {
2413
2663
  if (!args.all) rememberProjects(roots);
2414
2664
  }
2415
2665
  function update(args) {
2416
- const roots = args.all ? visibleProjects() : [findProjectRoot()];
2666
+ const roots = args.all ? visibleProjects() : [requireProject()];
2417
2667
  const results = roots.map((root) => {
2418
2668
  if (args.allowTracked) allowTrackedLinks(root);
2419
- const changes = updateProject(root, args.positional.length ? args.positional : void 0, { force: args.force });
2669
+ const names = args.positional.length ? args.positional : void 0;
2670
+ const changes = updateProject(root, names, { force: args.force });
2671
+ if (!args.all) {
2672
+ const { skills } = readManifest(root);
2673
+ for (const name of names ?? []) if (!(name in skills)) changes.push({ name, action: "skipped", reason: "not managed by skilllib in this project" });
2674
+ }
2675
+ if (changes.some((c) => c.action === "skipped")) process.exitCode = 1;
2420
2676
  if (!args.json) {
2421
2677
  if (args.all) info(`${basename8(root)} ${dim(tildify(root))}`);
2422
2678
  if (changes.length === 0) success("Everything is on the latest version");
@@ -2427,7 +2683,7 @@ function update(args) {
2427
2683
  if (args.json) json(args.all ? results : results[0].changes);
2428
2684
  }
2429
2685
  function outdated(args) {
2430
- const roots = args.all ? visibleProjects() : [findProjectRoot()];
2686
+ const roots = args.all ? visibleProjects() : [requireProject()];
2431
2687
  const behind = roots.flatMap(
2432
2688
  (root) => projectStatus(root).filter((s) => s.managed && s.latest !== null && s.version !== null && s.latest > s.version).map((s) => ({ project: basename8(root), path: root, skill: s.name, installed: s.version, latest: s.latest, state: s.state }))
2433
2689
  );
@@ -2439,7 +2695,7 @@ function outdated(args) {
2439
2695
  \u2192 skilllib update${args.all ? " --all" : ""}`));
2440
2696
  }
2441
2697
  function importSkills(args) {
2442
- const dirs = args.global ? machineSkills().filter((m) => m.movable && !m.broken).map((m) => m.path) : args.positional.map((p) => resolve4(p));
2698
+ const dirs = args.global ? machineSkills().filter((m) => m.movable && !m.broken).map((m) => m.path) : args.positional.map((p) => resolve7(p));
2443
2699
  if (dirs.length === 0) {
2444
2700
  error("Usage: skilllib import <skill-dir>... [--force] | skilllib import --global");
2445
2701
  process.exit(1);
@@ -2458,9 +2714,9 @@ function importSkills(args) {
2458
2714
  continue;
2459
2715
  }
2460
2716
  success(`${name} ${dim(result === "unchanged" ? "already in library" : result === "added" ? "added to library" : "library updated")}`);
2461
- const skillsDir = resolve4(dir, "..");
2462
- const root = resolve4(skillsDir, "..", "..");
2463
- if (skillsDir === join15(root, ".claude", "skills") && isProjectCandidate(root)) {
2717
+ const skillsDir = resolve7(dir, "..");
2718
+ const root = resolve7(skillsDir, "..", "..");
2719
+ if (skillsDir === join14(root, ".claude", "skills") && isProjectCandidate(root)) {
2464
2720
  if (readManifest(root).skills[name]?.hash !== treeHash(dir)) {
2465
2721
  const change = addSkill(root, name);
2466
2722
  if (change.action !== "skipped") info(dim(` ${basename8(root)} now depends on ${name} v${change.to}`));
@@ -2479,24 +2735,34 @@ function scan(args) {
2479
2735
  }
2480
2736
  const found = discoverProjects();
2481
2737
  const library = new Set(librarySkills().map((s) => s.name));
2482
- const skillNames = (root) => [...skillDirsIn(projectSkillsDir(root)), ...skillDirsIn(agentsSkillsDir(root))].map((d) => basename8(d));
2738
+ const skillNames = (root) => [
2739
+ .../* @__PURE__ */ new Set([...ALL_PROJECT_DIRS.flatMap((d) => skillDirsIn(join14(root, d))).map((d) => basename8(d)), ...nestedSkills(root).map((s) => s.name)])
2740
+ ];
2483
2741
  const withSkills = found.filter((root) => skillNames(root).length > 0);
2484
- info(`Scanned ${roots.map((r) => tildify(r)).join(", ")}: ${found.length} project(s), ${withSkills.length} with skills
2742
+ info(`Scanned ${roots.map((r) => tildify(r)).join(", ")}: ${found.length} project${found.length === 1 ? "" : "s"}, ${withSkills.length} with skills
2485
2743
  `);
2486
2744
  table(
2487
2745
  withSkills.map((root) => ({
2488
2746
  Project: basename8(root),
2489
2747
  Path: dim(tildify(root)),
2490
- Skills: [...new Set(skillNames(root))].map((n) => library.has(n) ? n : yellow(n)).join(", ")
2748
+ Skills: skillNames(root).map((n) => library.has(n) ? n : yellow(n)).join(", ")
2491
2749
  }))
2492
2750
  );
2493
2751
  if (withSkills.length > 0) info(`
2494
- ${yellow("Yellow")} skills aren't in your library yet (import them from \`skilllib\` \u2192 3 Sources or 5 Health).`);
2752
+ ${yellow("Yellow")} skills aren't in your library yet (\`skilllib import <skill-dir>\`, or \`skilllib\` \u2192 Health).`);
2495
2753
  }
2496
2754
  function folders(args) {
2497
2755
  const [action, ...paths] = args.positional;
2756
+ let failed = false;
2498
2757
  if (action === "add") {
2499
- for (const p of paths) success(`Added ${tildify(addRoot(p))}`);
2758
+ for (const p of paths) {
2759
+ try {
2760
+ success(`Added ${tildify(addRoot(p))}`);
2761
+ } catch (e) {
2762
+ error(e.message);
2763
+ failed = true;
2764
+ }
2765
+ }
2500
2766
  discoverProjects();
2501
2767
  } else if (action === "remove" || action === "rm") {
2502
2768
  for (const p of paths) {
@@ -2508,12 +2774,24 @@ function folders(args) {
2508
2774
  info(roots.length ? `Project folders:
2509
2775
  ${roots.map((r) => ` ${tildify(r)}`).join("\n")}` : "No project folders. Add one: skilllib folders add ~/Projects");
2510
2776
  if (hidden.length) info(dim(`Hidden projects: ${hidden.map((h) => tildify(h)).join(", ")} (skilllib unhide <path>)`));
2777
+ if (failed) process.exit(1);
2511
2778
  }
2512
2779
  function unhide(args) {
2780
+ if (args.positional.length === 0) {
2781
+ error("Usage: skilllib unhide <path>...");
2782
+ process.exit(1);
2783
+ }
2784
+ let failed = false;
2513
2785
  for (const p of args.positional) {
2514
- setHidden(expandHome(p), false);
2515
- success(`${tildify(expandHome(p))} is visible again`);
2786
+ const path = expandHome(p);
2787
+ if (setHidden(path, false)) success(`${tildify(path)} is visible again`);
2788
+ else {
2789
+ const { hidden } = readConfig();
2790
+ error(`${tildify(path)} wasn't hidden. ${hidden.length ? `Hidden projects: ${hidden.map((h) => tildify(h)).join(", ")}` : "No projects are hidden."}`);
2791
+ failed = true;
2792
+ }
2516
2793
  }
2794
+ if (failed) process.exit(1);
2517
2795
  }
2518
2796
  function newSkill(args) {
2519
2797
  const [name, ...rest] = args.positional;
@@ -2526,14 +2804,14 @@ function newSkill(args) {
2526
2804
  error(`Can't create ${name}: ${r.reason}`);
2527
2805
  process.exit(1);
2528
2806
  }
2529
- success(`Created ${tildify(join15(r.dir, "SKILL.md"))}`);
2807
+ success(`Created ${tildify(join14(r.dir, "SKILL.md"))}`);
2530
2808
  }
2531
2809
  function restore(args) {
2532
2810
  const backups = [...listBackups(), ...pluginBackups()];
2533
2811
  const name = args.positional[0];
2534
2812
  if (!name) {
2535
2813
  if (backups.length === 0) return info("Nothing to restore.");
2536
- table(backups.map((b) => ({ Skill: b.name, From: b.kind === "trash" ? "library (deleted)" : b.kind === "plugin" ? "Claude Code plugin" : tildify(dirname5(b.from)), Moved: b.movedAt })));
2814
+ table(backups.map((b) => ({ Skill: b.name, From: backupFrom(b), Moved: b.movedAt })));
2537
2815
  info(dim("\n\u2192 skilllib restore <name>"));
2538
2816
  return;
2539
2817
  }
@@ -2544,7 +2822,10 @@ function restore(args) {
2544
2822
  }
2545
2823
  const r = backup.kind === "plugin" ? restorePlugin(backup) : restoreBackup(backup);
2546
2824
  if (r.ok) success(`Restored ${name} to ${tildify(r.to)}`);
2547
- else error(`Can't restore ${name}: ${r.reason}`);
2825
+ else {
2826
+ error(`Can't restore ${name}: ${r.reason}`);
2827
+ process.exitCode = 1;
2828
+ }
2548
2829
  }
2549
2830
  async function doctor(args) {
2550
2831
  const projects2 = visibleProjects();
@@ -2564,13 +2845,18 @@ async function doctor(args) {
2564
2845
  })
2565
2846
  );
2566
2847
  }
2848
+ const fixed = (fix) => {
2849
+ const r = runFix(fix);
2850
+ if (!r.ok) process.exitCode = 1;
2851
+ return r;
2852
+ };
2567
2853
  if (args.json) {
2568
2854
  return json(
2569
2855
  issues.map(({ fix, choices, ...issue }) => ({
2570
2856
  ...issue,
2571
2857
  ...fix && { fix: fix.label },
2572
2858
  ...choices && { choices: choices.map((c) => c.label) },
2573
- ...args.fix && fix && { fixed: runFix(fix) }
2859
+ ...args.fix && fix && { fixed: fixed(fix) }
2574
2860
  }))
2575
2861
  );
2576
2862
  }
@@ -2580,7 +2866,7 @@ async function doctor(args) {
2580
2866
  info(dim(` ${issue.detail}${issue.fix ? ` \u2192 ${issue.fix.label}` : ""}`));
2581
2867
  if (issue.choices) info(dim(` choose in skilllib \u2192 Health: ${issue.choices.map((c) => c.label).join(" / ")}`));
2582
2868
  if (args.fix && issue.fix) {
2583
- const r = runFix(issue.fix);
2869
+ const r = fixed(issue.fix);
2584
2870
  info(` ${r.ok ? green("\u2713") : red("\u2717")} ${r.message}`);
2585
2871
  }
2586
2872
  }
@@ -2602,7 +2888,7 @@ async function usage(args) {
2602
2888
  return;
2603
2889
  }
2604
2890
  const library = new Set(librarySkills().map((s) => s.name));
2605
- info(`Skill usage, last ${args.days} days ${dim(`(from ${tildify(join15(claudeDir(), "projects"))})`)}
2891
+ info(`Skill usage, last ${args.days} days ${dim(`(from ${tildify(join14(claudeDir(), "projects"))})`)}
2606
2892
  `);
2607
2893
  table(
2608
2894
  summaries.map((s) => ({
@@ -2621,7 +2907,7 @@ async function usage(args) {
2621
2907
  }
2622
2908
  function show(args) {
2623
2909
  const name = args.positional[0];
2624
- const dir = name ? join15(libraryDir(), name) : "";
2910
+ const dir = name ? join14(libraryDir(), name) : "";
2625
2911
  if (!name || !isSkillDir(dir)) {
2626
2912
  error(name ? `${name} is not in the library` : "Usage: skilllib show <skill-name>");
2627
2913
  process.exit(1);
@@ -2635,7 +2921,7 @@ function show(args) {
2635
2921
  latest: latestVersion(name)?.version ?? null,
2636
2922
  origin: libraryOrigins()[name] ?? "",
2637
2923
  projects: knownProjects().filter((p) => name in readManifest(p).skills).map((p) => basename8(p)),
2638
- skillMd: readFileSync9(join15(dir, "SKILL.md"), "utf-8")
2924
+ skillMd: readFileSync9(join14(dir, "SKILL.md"), "utf-8")
2639
2925
  });
2640
2926
  }
2641
2927
  info(`${name} ${dim(tildify(dir))}`);
@@ -2664,14 +2950,21 @@ function harnessesCommand(args) {
2664
2950
  New installs go to ${installDirs(enabled).join(" (real copy) + ")}${installDirs(enabled).length > 1 ? " (link)" : ""}.`));
2665
2951
  }
2666
2952
  function link(args) {
2667
- const roots = args.all ? visibleProjects() : [findProjectRoot()];
2953
+ const roots = args.all ? visibleProjects() : [requireProject()];
2668
2954
  for (const root of roots) {
2669
2955
  if (args.allowTracked) allowTrackedLinks(root);
2670
2956
  const res = linkAll(root);
2671
2957
  const label = args.all ? `${basename8(root)}: ` : "";
2672
2958
  if (res.linked.length) success(`${label}linked ${res.linked.length} skill(s) for your other agents`);
2673
- else if (!args.all) success("Every skill here is already usable by all your agents");
2674
- if (res.blocked.length) warn(`${label}git tracks ${res.blocked.join(", ")}; re-run with --allow-tracked to link there too`);
2959
+ else if (!args.all && !res.blocked.length && !res.uncommitted.length) success("Every skill here is already usable by all your agents");
2960
+ if (res.blocked.length) {
2961
+ process.exitCode = 1;
2962
+ error(`${label}git tracks ${res.blocked.join(", ")}, so ${res.linked.length ? "some skills weren't" : "nothing was"} linked there; re-run with --allow-tracked to link there too`);
2963
+ }
2964
+ if (res.uncommitted.length)
2965
+ warn(
2966
+ `${label}${res.uncommitted.join(", ")} ${res.uncommitted.length === 1 ? "isn't" : "aren't"} committed, so not linked into folders git tracks (teammates would get broken links); commit ${res.uncommitted.length === 1 ? "it" : "them"} first`
2967
+ );
2675
2968
  }
2676
2969
  }
2677
2970
  function globalCommand(args) {
@@ -2692,14 +2985,41 @@ function globalCommand(args) {
2692
2985
  const kept = keptGlobal();
2693
2986
  if (!yours.length) return info("None of your skills load globally.");
2694
2987
  for (const m of yours) info(`${kept.has(m.name) ? green("\u2713") : yellow("\u26A0")} ${m.name.padEnd(32)} ${dim(tildify(m.path))}`);
2695
- const unreviewed = yours.filter((m) => !kept.has(m.name)).length;
2988
+ const unreviewed = new Set(yours.filter((m) => !kept.has(m.name)).map((m) => m.name)).size;
2696
2989
  if (unreviewed) info(dim(`
2697
2990
  ${unreviewed} not reviewed. Keep one global on purpose: skilllib global keep <name>`));
2698
2991
  }
2699
2992
  function tidy(args) {
2700
2993
  const names = args.positional;
2701
- const keepFor = (base) => args.keep ? Object.fromEntries(names.map((n) => [n, base ? resolve4(base, args.keep) : expandHome(args.keep)])) : {};
2702
- const reports = args.global ? [{ label: "global folders", report: planGlobalTidy({ keep: keepFor(null) }) }] : (args.all ? visibleProjects() : [findProjectRoot()]).map((root) => ({ label: basename8(root), report: planProjectTidy(root, { keep: keepFor(root) }) }));
2994
+ const keepFor = (base) => args.keep ? Object.fromEntries(names.map((n) => [n, base ? resolve7(base, args.keep) : expandHome(args.keep)])) : {};
2995
+ if (args.keep && !names.length) {
2996
+ error("Usage: skilllib tidy <name>... --keep <folder>");
2997
+ process.exit(1);
2998
+ }
2999
+ const targets = args.global ? [null] : args.all ? visibleProjects() : [requireProject()];
3000
+ const problems = [];
3001
+ for (const name of names) {
3002
+ const where = targets.map((root) => ({ root, ...foldersOf(root, name) })).filter((w) => w.all.length);
3003
+ if (!where.length) {
3004
+ problems.push(`${name}: no skill by that name ${args.global ? "in your global folders" : args.all ? "in your projects" : `in ${basename8(targets[0])}`}`);
3005
+ continue;
3006
+ }
3007
+ if (!args.keep) continue;
3008
+ for (const { root, real } of where) {
3009
+ if (real.includes(keepFor(root)[name])) continue;
3010
+ const label = root ? `${basename8(root)} \xB7 ` : "";
3011
+ problems.push(
3012
+ `${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"}`
3013
+ );
3014
+ }
3015
+ }
3016
+ if (problems.length) {
3017
+ for (const p of problems) error(p);
3018
+ process.exit(1);
3019
+ }
3020
+ const reports = targets.map(
3021
+ (root) => root === null ? { label: "global folders", report: planGlobalTidy({ keep: keepFor(null) }) } : { label: basename8(root), report: planProjectTidy(root, { keep: keepFor(root) }) }
3022
+ );
2703
3023
  let held = 0;
2704
3024
  let changed = 0;
2705
3025
  for (const { label, report } of reports) {
@@ -2714,6 +3034,7 @@ function tidy(args) {
2714
3034
  held += args.dryRun ? 0 : result.held.length;
2715
3035
  changed += result.applied.length;
2716
3036
  }
3037
+ if (args.keep && conflicts.length) process.exitCode = 1;
2717
3038
  for (const c of conflicts) {
2718
3039
  warn(`${label} \xB7 ${c.name}: the copies differ`);
2719
3040
  for (const copy of c.copies) {
@@ -2756,16 +3077,19 @@ function agentSkill(args) {
2756
3077
  }
2757
3078
 
2758
3079
  export {
2759
- userHome,
2760
3080
  skilllibHome,
3081
+ PROJECT_SKILLS_DIR,
2761
3082
  HARNESSES,
2762
3083
  harness,
2763
3084
  readSkillInfo,
2764
3085
  versionHistory,
2765
3086
  forgetLatest,
2766
3087
  latestVersion,
2767
- findProjectRoot,
2768
- isProjectCandidate,
3088
+ projectHere,
3089
+ readManifest,
3090
+ setJsonMode,
3091
+ error,
3092
+ tildify,
2769
3093
  readConfig,
2770
3094
  addRoot,
2771
3095
  removeRoot,
@@ -2785,6 +3109,7 @@ export {
2785
3109
  recordOrigin,
2786
3110
  librarySkills,
2787
3111
  librarySkillDir,
3112
+ visibilityOf,
2788
3113
  projectStatus,
2789
3114
  linkEverywhere,
2790
3115
  relinkDependency,
@@ -2802,8 +3127,6 @@ export {
2802
3127
  createSkill,
2803
3128
  listBackups,
2804
3129
  restoreBackup,
2805
- setJsonMode,
2806
- error,
2807
3130
  gitInfo,
2808
3131
  relativeTo,
2809
3132
  claudePlugins,
@@ -2813,6 +3136,9 @@ export {
2813
3136
  pluginBackups,
2814
3137
  restorePlugin,
2815
3138
  cursorPluginSkills,
3139
+ agentSkillState,
3140
+ installAgentSkill,
3141
+ removeAgentSkill,
2816
3142
  refreshAgentSkill,
2817
3143
  planProjectTidy,
2818
3144
  planGlobalTidy,
@@ -2820,10 +3146,12 @@ export {
2820
3146
  applyTidy,
2821
3147
  describeStep,
2822
3148
  copyLabel,
3149
+ addedAt,
2823
3150
  scanUsage,
2824
3151
  parseArgs,
2825
3152
  projectOfFactory,
2826
3153
  status,
3154
+ overview,
2827
3155
  list,
2828
3156
  projects,
2829
3157
  add,