skilllib 1.3.0 → 1.6.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,14 +1,22 @@
1
1
  // src/output.ts
2
2
  import { sep } from "path";
3
3
  import { homedir } from "os";
4
- var color = (code) => (s) => process.stdout.isTTY ? `\x1B[${code}m${s}\x1B[0m` : s;
4
+ var jsonMode = false;
5
+ function setJsonMode(on) {
6
+ jsonMode = on;
7
+ }
8
+ function json(data) {
9
+ process.stdout.write(JSON.stringify(data) + "\n");
10
+ }
11
+ var color = (code) => (s) => process.stdout.isTTY && !jsonMode ? `\x1B[${code}m${s}\x1B[0m` : s;
5
12
  var green = color(32);
6
13
  var yellow = color(33);
7
14
  var red = color(31);
8
15
  var dim = color(2);
9
- var info = (msg) => console.log(msg);
10
- var success = (msg) => console.log(`${green("\u2713")} ${msg}`);
11
- var warn = (msg) => console.log(`${yellow("!")} ${msg}`);
16
+ var out = (msg) => jsonMode ? console.error(msg) : console.log(msg);
17
+ var info = (msg) => out(msg);
18
+ var success = (msg) => out(`${green("\u2713")} ${msg}`);
19
+ var warn = (msg) => out(`${yellow("!")} ${msg}`);
12
20
  var error = (msg) => console.error(`${red("\u2717")} ${msg}`);
13
21
  function tildify(path) {
14
22
  const home2 = homedir();
@@ -28,13 +36,14 @@ function table(rows) {
28
36
  for (const row of rows) info(keys.map((k, i) => pad(row[k] ?? "", widths[i])).join(" ").trimEnd());
29
37
  }
30
38
 
31
- // src/commands.ts
32
- import { basename as basename6, dirname as dirname5, join as join13, resolve as resolve4, sep as sep5 } from "path";
39
+ // src/agentSkill.ts
40
+ import { existsSync as existsSync8, lstatSync as lstatSync3, mkdirSync as mkdirSync6, readFileSync as readFileSync7, rmSync as rmSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "fs";
41
+ import { join as join9 } from "path";
33
42
 
34
- // src/library.ts
35
- import { execFileSync } from "child_process";
36
- 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";
37
- import { basename as basename3, dirname as dirname2, join as join8, relative as relative2, sep as sep3 } from "path";
43
+ // 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";
38
47
 
39
48
  // src/paths.ts
40
49
  import { homedir as homedir2 } from "os";
@@ -51,9 +60,16 @@ function libraryDir() {
51
60
  function claudeDir() {
52
61
  return process.env.CLAUDE_CONFIG_DIR ?? join(userHome(), ".claude");
53
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
+ }
54
69
  var PROJECT_SKILLS_DIR = ".claude/skills";
55
70
  var AGENTS_SKILLS_DIR = ".agents/skills";
56
71
  var MANIFEST_FILE = "skilllib.json";
72
+ var OWN_SKILL_MARKER = ".managed-by-skilllib";
57
73
 
58
74
  // src/harnesses.ts
59
75
  import { existsSync } from "fs";
@@ -76,8 +92,11 @@ var HARNESSES = [
76
92
  short: "Cu",
77
93
  icon: "\u2B21",
78
94
  color: "#E8E8E8",
95
+ // The first folder is where links for Cursor go (see linkEverywhere). Cursor also reads .grok/skills;
96
+ // skilllib leaves that folder to Grok, since it can't tell what Grok needs.
79
97
  projectDirs: [".agents/skills", ".cursor/skills", ".claude/skills", ".codex/skills"],
80
- globalDirs: () => [join2(home(), ".agents", "skills"), join2(home(), ".cursor", "skills"), join2(claudeDir(), "skills"), join2(home(), ".codex", "skills")],
98
+ // Cursor ignores CLAUDE_CONFIG_DIR and CODEX_HOME: it always reads these under your home folder.
99
+ globalDirs: () => [".agents", ".cursor", ".claude", ".codex"].map((d) => join2(home(), d, "skills")),
81
100
  installed: () => existsSync(join2(home(), ".cursor")) || onPath("cursor-agent") || hasApp("Cursor")
82
101
  },
83
102
  {
@@ -86,9 +105,9 @@ var HARNESSES = [
86
105
  short: "Cx",
87
106
  icon: "\u25CE",
88
107
  color: "#10A37F",
89
- projectDirs: [".agents/skills"],
90
- globalDirs: () => [join2(home(), ".agents", "skills"), "/etc/codex/skills"],
91
- installed: () => existsSync(join2(home(), ".codex")) || onPath("codex") || hasApp("Codex")
108
+ projectDirs: [".agents/skills", ".codex/skills"],
109
+ globalDirs: () => [join2(home(), ".agents", "skills"), join2(codexHome(), "skills"), codexSystemDir()],
110
+ installed: () => existsSync(codexHome()) || onPath("codex") || hasApp("Codex")
92
111
  },
93
112
  {
94
113
  id: "zed",
@@ -111,6 +130,7 @@ function hasApp(name) {
111
130
  function harness(id) {
112
131
  return HARNESSES.find((h) => h.id === id);
113
132
  }
133
+ var CURSOR_PICKS = [".cursor", ".claude", ".codex", ".grok", ".agents"];
114
134
  var ALL_PROJECT_DIRS = [...new Set(HARNESSES.flatMap((h) => h.projectDirs))];
115
135
  var PREFERRED_DIRS = [AGENTS_SKILLS_DIR, PROJECT_SKILLS_DIR];
116
136
  function preference(dir) {
@@ -133,11 +153,6 @@ function installDirs(enabled, keep) {
133
153
  return [primary, ...best.filter((d) => d !== primary)];
134
154
  }
135
155
 
136
- // src/config.ts
137
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
138
- import { homedir as homedir4 } from "os";
139
- import { join as join6, resolve as resolve2 } from "path";
140
-
141
156
  // src/project.ts
142
157
  import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
143
158
  import { homedir as homedir3 } from "os";
@@ -249,12 +264,10 @@ function getVersion(name, version) {
249
264
  // src/project.ts
250
265
  function findProjectRoot(from = process.cwd()) {
251
266
  let dir = resolve(from);
252
- let gitRoot = null;
253
267
  while (true) {
254
- if (existsSync4(join5(dir, MANIFEST_FILE))) return dir;
255
- if (!gitRoot && existsSync4(join5(dir, ".git"))) gitRoot = dir;
268
+ if (existsSync4(join5(dir, MANIFEST_FILE)) || existsSync4(join5(dir, ".git"))) return dir;
256
269
  const parent = dirname(dir);
257
- if (parent === dir) return gitRoot ?? resolve(from);
270
+ if (parent === dir) return resolve(from);
258
271
  dir = parent;
259
272
  }
260
273
  }
@@ -403,6 +416,11 @@ function setKeepGlobal(names, keep) {
403
416
  writeConfig({ ...readConfig(), keepGlobal: [...kept].sort() });
404
417
  }
405
418
 
419
+ // src/library.ts
420
+ 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";
422
+ import { basename as basename3, dirname as dirname2, join as join8, relative as relative2, sep as sep3 } from "path";
423
+
406
424
  // src/sources.ts
407
425
  import { existsSync as existsSync6, lstatSync, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync5, readlinkSync, realpathSync as realpathSync2, writeFileSync as writeFileSync4 } from "fs";
408
426
  import { basename as basename2, join as join7, resolve as resolve3 } from "path";
@@ -429,8 +447,24 @@ function realpathOrNull(path) {
429
447
  function harnessesReading(dir, enabled) {
430
448
  return enabled.filter((id) => HARNESSES.find((h) => h.id === id)?.globalDirs().includes(dir));
431
449
  }
450
+ var systemDirs = () => [codexSystemDir()];
432
451
  function globalSkillDirs() {
433
- return [...new Set(HARNESSES.flatMap((h) => h.globalDirs()))].filter((d) => d.startsWith(userHome()));
452
+ return [...new Set(HARNESSES.flatMap((h) => h.globalDirs()))].filter((d) => !systemDirs().includes(d));
453
+ }
454
+ function systemSkills(enabled) {
455
+ return systemDirs().flatMap(
456
+ (dir) => skillDirsIn(dir).map((path) => ({
457
+ name: basename2(path),
458
+ kind: "system",
459
+ origin: dir.replace(/\\/g, "/"),
460
+ path,
461
+ description: readSkillInfo(path).description,
462
+ movable: false,
463
+ broken: false,
464
+ harnesses: harnessesReading(dir, enabled),
465
+ links: []
466
+ }))
467
+ ).filter((s) => s.harnesses.length > 0);
434
468
  }
435
469
  function globalFolderSkills(enabled) {
436
470
  const lock = skillsShLock();
@@ -457,6 +491,10 @@ function globalFolderSkills(enabled) {
457
491
  const target_ = e.isLink ? readlinkSync(e.path).replace(/\\/g, "/") : "";
458
492
  const inAgents = e.dir === agentsDir || target_.includes(".agents/skills");
459
493
  const locked = lock[e.name]?.source;
494
+ 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: [] });
496
+ continue;
497
+ }
460
498
  skills.push({
461
499
  name: e.name,
462
500
  kind: locked || e.isLink && inAgents ? "skills.sh" : "global",
@@ -495,9 +533,10 @@ function cursorBuiltInSkills(enabled) {
495
533
  links: []
496
534
  }));
497
535
  }
498
- function claudeAiSkills() {
536
+ function claudeAiSkills(enabled) {
499
537
  const synced = join7(claudeDir(), "skills", "synced");
500
- if (!existsSync6(synced)) return [];
538
+ const harnesses = harnessesReading(join7(claudeDir(), "skills"), enabled);
539
+ if (!existsSync6(synced) || harnesses.length === 0) return [];
501
540
  return readdirSync3(synced).filter((d) => !d.startsWith(".")).flatMap(
502
541
  (bucket) => skillDirsIn(join7(synced, bucket)).map((path) => ({
503
542
  name: basename2(path),
@@ -507,7 +546,7 @@ function claudeAiSkills() {
507
546
  description: readSkillInfo(path).description,
508
547
  movable: false,
509
548
  broken: false,
510
- harnesses: ["claude-code"],
549
+ harnesses,
511
550
  links: []
512
551
  }))
513
552
  );
@@ -520,11 +559,22 @@ function setPluginEnabled(id, on) {
520
559
  mkdirSync4(claudeDir(), { recursive: true });
521
560
  writeFileSync4(file, JSON.stringify(settings, null, 2) + "\n");
522
561
  }
523
- function pluginSkills() {
562
+ function repoSettingsFiles(root) {
563
+ return [join7(root, ".claude", "settings.json"), join7(root, ".claude", "settings.local.json")];
564
+ }
565
+ function enabledPlugins(root) {
566
+ const files = [join7(claudeDir(), "settings.json"), ...root ? repoSettingsFiles(root) : []];
567
+ return Object.assign({}, ...files.map((f) => readJson(f)?.enabledPlugins ?? {}));
568
+ }
569
+ function pluginSkills(harnesses, root) {
570
+ const claude2 = harnesses.includes("claude-code");
571
+ const cursor = harnesses.includes("cursor");
572
+ if (!claude2 && !cursor) return [];
524
573
  const plugins = join7(claudeDir(), "plugins");
525
- const enabled = readJson(join7(claudeDir(), "settings.json"))?.enabledPlugins ?? {};
526
574
  const installed = readJson(join7(plugins, "installed_plugins.json"))?.plugins ?? {};
527
- const fromMarketplaces = Object.entries(enabled).filter(([, on]) => on).flatMap(([id]) => {
575
+ const user = enabledPlugins();
576
+ const enabled = root ? enabledPlugins(root) : user;
577
+ const fromMarketplaces = [.../* @__PURE__ */ new Set([...Object.keys(enabled), ...Object.keys(user)])].filter((id) => claude2 && enabled[id] || cursor && user[id]).flatMap((id) => {
528
578
  const [name = "", marketplace = ""] = id.split("@");
529
579
  const candidates = [
530
580
  ...(installed[id] ?? []).flatMap((i) => i.installPath ? [i.installPath] : []),
@@ -532,19 +582,22 @@ function pluginSkills() {
532
582
  join7(plugins, "marketplaces", marketplace, "external_plugins", name),
533
583
  join7(plugins, "marketplaces", marketplace)
534
584
  ];
535
- const root = candidates.find((c) => existsSync6(join7(c, "skills")));
536
- return root ? skillDirsIn(join7(root, "skills")).map((path) => ({ path, origin: id })) : [];
585
+ const dir = candidates.find((c) => existsSync6(join7(c, "skills")));
586
+ if (!dir) return [];
587
+ const inCursor = cursor && user[id] && (installed[id] ?? []).some((i) => i.scope === "user" && i.installPath === dir);
588
+ const loadedBy = [...claude2 && enabled[id] ? ["claude-code"] : [], ...inCursor ? ["cursor"] : []];
589
+ return loadedBy.length ? skillDirsIn(join7(dir, "skills")).map((path) => ({ path, origin: id, loadedBy })) : [];
537
590
  });
538
591
  const syncedDir = join7(plugins, "synced");
539
- const fromSynced = existsSync6(syncedDir) ? readdirSync3(syncedDir).filter((b) => !b.startsWith(".")).flatMap(
592
+ const fromSynced = claude2 && existsSync6(syncedDir) ? readdirSync3(syncedDir).filter((b) => !b.startsWith(".")).flatMap(
540
593
  (bucket) => readdirSync3(join7(syncedDir, bucket), { withFileTypes: true }).filter((d) => d.isDirectory()).flatMap((d) => {
541
- const root = join7(syncedDir, bucket, d.name);
542
- const name = readJson(join7(root, ".claude-plugin", "plugin.json"))?.name ?? d.name;
594
+ const root2 = join7(syncedDir, bucket, d.name);
595
+ const name = readJson(join7(root2, ".claude-plugin", "plugin.json"))?.name ?? d.name;
543
596
  if (enabled[`${name}@synced`] === false) return [];
544
- return skillDirsIn(join7(root, "skills")).map((path) => ({ path, origin: `${name}@synced` }));
597
+ return skillDirsIn(join7(root2, "skills")).map((path) => ({ path, origin: `${name}@synced`, loadedBy: ["claude-code"] }));
545
598
  })
546
599
  ) : [];
547
- return [...fromMarketplaces, ...fromSynced].map(({ path, origin }) => ({
600
+ return [...fromMarketplaces, ...fromSynced].map(({ path, origin, loadedBy }) => ({
548
601
  name: basename2(path),
549
602
  kind: "plugin",
550
603
  origin,
@@ -552,20 +605,24 @@ function pluginSkills() {
552
605
  description: readSkillInfo(path).description,
553
606
  movable: false,
554
607
  broken: false,
555
- harnesses: ["claude-code"],
608
+ harnesses: loadedBy,
556
609
  links: []
557
610
  }));
558
611
  }
612
+ var byKindAndName = (a, b) => a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name);
559
613
  function machineSkills(enabled = enabledHarnesses()) {
560
- const claude2 = enabled.includes("claude-code");
561
614
  return [
562
615
  ...globalFolderSkills(enabled),
563
- ...claude2 ? claudeAiSkills() : [],
564
- ...claude2 ? pluginSkills() : [],
565
- ...cursorBuiltInSkills(enabled)
566
- ].sort(
567
- (a, b) => a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name)
568
- );
616
+ ...claudeAiSkills(enabled),
617
+ ...pluginSkills(enabled),
618
+ ...cursorBuiltInSkills(enabled),
619
+ ...systemSkills(enabled)
620
+ ].sort(byKindAndName);
621
+ }
622
+ function skillsLoadedIn(root, machine, enabled = enabledHarnesses()) {
623
+ if (!enabled.includes("claude-code")) return machine;
624
+ if (!repoSettingsFiles(root).some((f) => readJson(f)?.enabledPlugins)) return machine;
625
+ return [...machine.filter((m) => m.kind !== "plugin"), ...pluginSkills(enabled, root)].sort(byKindAndName);
569
626
  }
570
627
  function originsFile() {
571
628
  return join7(skilllibHome(), "origins.json");
@@ -634,8 +691,8 @@ function dependencyDirs(root, name, dep) {
634
691
  }
635
692
  function isGitTracked(root, dir) {
636
693
  try {
637
- const out = execFileSync("git", ["ls-files", "--", dir], { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
638
- return out.trim().length > 0;
694
+ const out2 = execFileSync("git", ["ls-files", "--", dir], { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
695
+ return out2.trim().length > 0;
639
696
  } catch {
640
697
  return false;
641
698
  }
@@ -676,18 +733,13 @@ function projectStatus(root) {
676
733
  for (const [name, paths] of byName) {
677
734
  const real = paths.find((p) => !isLink(p)) ?? paths[0];
678
735
  const location = relative2(root, join8(real, "..")).split(sep3).join("/");
679
- const latest = latestVersion(name);
680
- const local = treeHash(real);
681
- const sameAsLibrary = latest !== null && (latest.hash === local || versionForHash(name, local ?? "") !== null);
682
- const committed = location === AGENTS_SKILLS_DIR;
683
736
  const source = lock[name]?.source;
684
- const state = source ? "from npx skills" : committed ? latest === null ? "repo skill" : sameAsLibrary ? "repo skill, in library" : "repo skill, differs from library" : latest === null ? "local only" : sameAsLibrary ? "untracked copy of library skill" : "untracked, differs from library";
685
737
  unmanaged.push({
686
738
  name,
687
739
  managed: false,
688
- state,
740
+ state: unmanagedState(name, real, location === AGENTS_SKILLS_DIR, source),
689
741
  version: null,
690
- latest: latest?.version ?? null,
742
+ latest: latestVersion(name)?.version ?? null,
691
743
  location,
692
744
  path: real,
693
745
  visibility: visibilityOf(root, name, enabled),
@@ -696,6 +748,55 @@ function projectStatus(root) {
696
748
  }
697
749
  return [...managed, ...unmanaged].sort((a, b) => a.name.localeCompare(b.name));
698
750
  }
751
+ function unmanagedState(name, real, committed, source) {
752
+ if (source) return "from npx skills";
753
+ const latest = latestVersion(name);
754
+ const local = treeHash(real);
755
+ const sameAsLibrary = latest !== null && (latest.hash === local || versionForHash(name, local ?? "") !== null);
756
+ if (committed) return latest === null ? "repo skill" : sameAsLibrary ? "repo skill, in library" : "repo skill, differs from library";
757
+ return latest === null ? "local only" : sameAsLibrary ? "untracked copy of library skill" : "untracked, differs from library";
758
+ }
759
+ var NESTED_READERS = [
760
+ [PROJECT_SKILLS_DIR, ["claude-code", "cursor"]],
761
+ [AGENTS_SKILLS_DIR, ["codex", "cursor"]],
762
+ [".cursor/skills", ["cursor"]],
763
+ [".codex/skills", ["codex", "cursor"]]
764
+ ];
765
+ var NESTED_SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "vendor", "target", "coverage"]);
766
+ function nestedSkills(root, { depth = 3, enabled = enabledHarnesses() } = {}) {
767
+ const found = [];
768
+ const walk = (dir, left) => {
769
+ let subs = [];
770
+ try {
771
+ subs = readdirSync4(dir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".") && !NESTED_SKIP.has(d.name)).map((d) => join8(dir, d.name));
772
+ } catch {
773
+ return;
774
+ }
775
+ for (const sub of subs) {
776
+ if (existsSync7(join8(sub, ".git"))) continue;
777
+ for (const [skillsDir, readers] of NESTED_READERS) {
778
+ const location = relative2(root, join8(sub, skillsDir)).split(sep3).join("/");
779
+ for (const path of skillDirsIn(join8(sub, skillsDir))) {
780
+ if (isLink(path) && (realpathOrNull2(path) ?? "").startsWith((realpathOrNull2(root) ?? root) + sep3)) continue;
781
+ const name = basename3(path);
782
+ found.push({
783
+ name,
784
+ managed: false,
785
+ state: unmanagedState(name, path, skillsDir === AGENTS_SKILLS_DIR, void 0),
786
+ version: null,
787
+ latest: latestVersion(name)?.version ?? null,
788
+ location,
789
+ path,
790
+ visibility: enabled.map((id) => ({ id, paths: readers.includes(id) ? 1 : 0 }))
791
+ });
792
+ }
793
+ }
794
+ if (left > 1) walk(sub, left - 1);
795
+ }
796
+ };
797
+ walk(root, depth);
798
+ return found.sort((a, b) => a.location.localeCompare(b.location) || a.name.localeCompare(b.name));
799
+ }
699
800
  function linkEverywhere(root, name, location, { allowTracked = false } = {}) {
700
801
  const enabled = enabledHarnesses();
701
802
  const result = { created: [], blocked: [] };
@@ -827,6 +928,14 @@ function syncProject(root, { force = false } = {}) {
827
928
  return change.action === "skipped" ? [change] : [{ ...change, action: local === null ? "installed" : "updated" }];
828
929
  });
829
930
  }
931
+ function discardEdits(root, name) {
932
+ const dep = readManifest(root).skills[name];
933
+ if (!dep) return { name, action: "skipped", reason: "not managed by skilllib in this project" };
934
+ const dir = join8(root, dep.dir ?? PROJECT_SKILLS_DIR, name);
935
+ const pinned = dep.version > 0 && getVersion(name, dep.version) ? dep.version : void 0;
936
+ if (existsSync7(dir) && !isLink(dir)) stash(dir, "edit-backup");
937
+ return addSkill(root, name, { force: true, version: pinned });
938
+ }
830
939
  function updateProject(root, names, { force = false } = {}) {
831
940
  const { skills } = readManifest(root);
832
941
  return Object.keys(skills).filter((name) => !names || names.includes(name)).flatMap((name) => {
@@ -847,9 +956,9 @@ function importSkill(dir, { force = false } = {}) {
847
956
  if (existing === incoming) return { name, status: "unchanged" };
848
957
  if (existing !== null && !force) return { name, status: "exists" };
849
958
  mkdirSync5(libraryDir(), { recursive: true });
850
- const origin = originFor(dir);
959
+ const origin = existing !== null && libraryOrigins()[name] ? null : originFor(dir);
851
960
  copySkill(dir, to);
852
- recordOrigin(name, origin);
961
+ if (origin) recordOrigin(name, origin);
853
962
  forgetLatest(name);
854
963
  latestVersion(name);
855
964
  return { name, status: existing === null ? "added" : "updated" };
@@ -903,6 +1012,23 @@ function unloadGlobal(path, links = [], { requireLibrary = true } = {}) {
903
1012
  if (!globalSkillDirs().some((d) => entryExists(join8(d, name)))) setKeepGlobal([name], false);
904
1013
  return { ok: true, movedTo };
905
1014
  }
1015
+ function linkGlobal(path, agents) {
1016
+ const name = basename3(path);
1017
+ const linked = [];
1018
+ const skipped = [];
1019
+ for (const dir of new Set(agents.map((id) => harness(id).globalDirs()[0]))) {
1020
+ const link2 = join8(dir, name);
1021
+ if (isLink(link2) && realpathOrNull2(link2) === null) unlinkSync(link2);
1022
+ else if (entryExists(link2)) {
1023
+ if (realpathOrNull2(link2) !== realpathOrNull2(path)) skipped.push(link2);
1024
+ continue;
1025
+ }
1026
+ mkdirSync5(dir, { recursive: true });
1027
+ linkDir(path, link2);
1028
+ linked.push(link2);
1029
+ }
1030
+ return { linked, skipped };
1031
+ }
906
1032
  function createSkill(name, description) {
907
1033
  if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) return { ok: false, reason: "use lowercase letters, digits and dashes" };
908
1034
  const dir = librarySkillDir(name);
@@ -924,7 +1050,7 @@ Instructions for Claude go here.
924
1050
  return { ok: true, dir };
925
1051
  }
926
1052
  function listBackups() {
927
- return ["trash", "global-backup", "tidy-backup"].flatMap((kind) => {
1053
+ return ["trash", "global-backup", "tidy-backup", "edit-backup"].flatMap((kind) => {
928
1054
  const dir = join8(skilllibHome(), kind);
929
1055
  if (!existsSync7(dir)) return [];
930
1056
  const index = readBackupIndex(kind);
@@ -940,6 +1066,7 @@ function listBackups() {
940
1066
  function restoreBackup(backup) {
941
1067
  const to = backup.from;
942
1068
  if (backup.kind === "tidy-backup" && isLink(to)) unlinkSync(to);
1069
+ if (backup.kind === "edit-backup" && !isLink(to) && versionForHash(backup.name, treeHash(to) ?? "") !== null) rmSync2(to, { recursive: true, force: true });
943
1070
  let taken = existsSync7(to);
944
1071
  try {
945
1072
  taken = taken || lstatSync2(to) !== null;
@@ -952,125 +1079,76 @@ function restoreBackup(backup) {
952
1079
  return { ok: true, to };
953
1080
  }
954
1081
 
955
- // src/usage.ts
956
- import { createReadStream, existsSync as existsSync8, readdirSync as readdirSync5, statSync as statSync2 } from "fs";
957
- import { join as join9 } from "path";
958
- import { createInterface } from "readline";
959
- var BUILTIN_COMMANDS = /* @__PURE__ */ new Set([
960
- "add-dir",
961
- "agents",
962
- "bug",
963
- "clear",
964
- "compact",
965
- "config",
966
- "context",
967
- "cost",
968
- "doctor",
969
- "exit",
970
- "export",
971
- "fast",
972
- "feedback",
973
- "goal",
974
- "help",
975
- "hooks",
976
- "ide",
977
- "init",
978
- "install-github-app",
979
- "login",
980
- "logout",
981
- "mcp",
982
- "memory",
983
- "model",
984
- "output-style",
985
- "permissions",
986
- "plugin",
987
- "pr-comments",
988
- "privacy-settings",
989
- "release-notes",
990
- "resume",
991
- "rewind",
992
- "status",
993
- "statusline",
994
- "terminal-setup",
995
- "todos",
996
- "upgrade",
997
- "usage",
998
- "vim"
999
- ]);
1000
- function skillNameFromPath(filePath) {
1001
- const match = filePath.replace(/\\/g, "/").match(/\/\.(?:claude|agents)\/skills\/([^/]+)\//);
1002
- return match ? match[1] : null;
1082
+ // src/agentSkill.ts
1083
+ var AGENT_SKILL_NAME = "skilllib";
1084
+ function shippedSkill() {
1085
+ return readFileSync7(new URL("../skills/skilllib/SKILL.md", import.meta.url), "utf-8");
1003
1086
  }
1004
- function usesFromTranscriptLine(line) {
1005
- if (!line.includes('"Skill"') && !line.includes("<command-name>") && !line.includes("/skills/")) return [];
1006
- let record;
1007
- try {
1008
- record = JSON.parse(line);
1009
- } catch {
1010
- return [];
1011
- }
1012
- const cwd = record.cwd ?? "";
1013
- const at = record.timestamp ?? "";
1014
- const content = record.message?.content;
1015
- if (record.type === "assistant" && Array.isArray(content)) {
1016
- return content.flatMap((block) => {
1017
- if (!block || typeof block !== "object" || block.type !== "tool_use") return [];
1018
- const key = String(block.id);
1019
- if (block.name === "Skill" && typeof block.input?.skill === "string") {
1020
- return [{ skill: block.input.skill, source: "invoked", cwd, at, key }];
1021
- }
1022
- if (block.name === "Read" && typeof block.input?.file_path === "string") {
1023
- const skill = skillNameFromPath(block.input.file_path);
1024
- return skill ? [{ skill, source: "file read", cwd, at, key }] : [];
1025
- }
1026
- return [];
1027
- });
1028
- }
1029
- if (record.type === "user" && typeof content === "string") {
1030
- const match = content.match(/<command-name>\/?([\w.:-]+)<\/command-name>/);
1031
- if (!match || BUILTIN_COMMANDS.has(match[1])) return [];
1032
- return [{ skill: match[1], source: "slash", cwd, at, key: record.uuid ?? `${at}:${match[1]}` }];
1033
- }
1034
- return [];
1087
+ function agentSkillDirs(enabled = enabledHarnesses()) {
1088
+ const claudeSkills = join9(claudeDir(), "skills");
1089
+ const agentsSkills = join9(userHome(), ".agents", "skills");
1090
+ const needsClaude = enabled.some((id) => harness(id).globalDirs().includes(claudeSkills));
1091
+ const needsAgents = enabled.some((id) => !harness(id).globalDirs().includes(claudeSkills));
1092
+ return [...needsClaude ? [claudeSkills] : [], ...needsAgents ? [agentsSkills] : []].map((d) => join9(d, AGENT_SKILL_NAME));
1035
1093
  }
1036
- function transcriptFiles(sinceMs) {
1037
- const base = join9(claudeDir(), "projects");
1038
- if (!existsSync8(base)) return [];
1039
- return readdirSync5(base).flatMap((project) => {
1040
- const dir = join9(base, project);
1041
- if (!statSync2(dir).isDirectory()) return [];
1042
- return readdirSync5(dir).filter((f) => f.endsWith(".jsonl")).map((f) => join9(dir, f)).filter((f) => statSync2(f).mtimeMs >= sinceMs);
1043
- });
1094
+ function isOurs(dir) {
1095
+ return existsSync8(join9(dir, OWN_SKILL_MARKER));
1044
1096
  }
1045
- async function scanUsage(days) {
1046
- const sinceMs = Date.now() - days * 24 * 60 * 60 * 1e3;
1047
- const seen = /* @__PURE__ */ new Map();
1048
- for (const file of transcriptFiles(sinceMs)) {
1049
- const lines = createInterface({ input: createReadStream(file), crlfDelay: Infinity });
1050
- for await (const line of lines) {
1051
- for (const use of usesFromTranscriptLine(line)) {
1052
- if (Date.parse(use.at) >= sinceMs) seen.set(use.key, use);
1053
- }
1054
- }
1097
+ function isDangling(path) {
1098
+ return entryExists2(path) && lstatSync3(path).isSymbolicLink() && !existsSync8(path);
1099
+ }
1100
+ function entryExists2(path) {
1101
+ try {
1102
+ lstatSync3(path);
1103
+ return true;
1104
+ } catch {
1105
+ return false;
1055
1106
  }
1056
- return [...seen.values()];
1057
1107
  }
1058
- function summarize(uses, projectOf = () => null) {
1059
- const bySkill = /* @__PURE__ */ new Map();
1060
- for (const use of uses) {
1061
- const skill = use.skill.split(":").pop();
1062
- const summary = bySkill.get(skill) ?? { skill, uses: 0, lastUsed: "", projects: /* @__PURE__ */ new Set() };
1063
- summary.uses += 1;
1064
- if (use.at > summary.lastUsed) summary.lastUsed = use.at;
1065
- const project = projectOf(use.cwd);
1066
- if (project) summary.projects.add(project);
1067
- bySkill.set(skill, summary);
1108
+ function agentSkillState(enabled = enabledHarnesses()) {
1109
+ const [primary, ...links] = agentSkillDirs(enabled);
1110
+ if (!primary || !isOurs(primary) || links.some((l) => !existsSync8(join9(l, "SKILL.md")))) return "missing";
1111
+ return readFileSync7(join9(primary, "SKILL.md"), "utf-8") === shippedSkill() ? "installed" : "outdated";
1112
+ }
1113
+ function installAgentSkill(enabled = enabledHarnesses()) {
1114
+ const [primary, ...links] = agentSkillDirs(enabled);
1115
+ if (!primary) return { ok: false, reason: "no agents chosen yet (skilllib harnesses <id>...)" };
1116
+ 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` };
1118
+ for (const d of [primary, ...links]) if (isDangling(d)) unlinkSync2(d);
1119
+ mkdirSync6(primary, { recursive: true });
1120
+ writeFileSync6(join9(primary, "SKILL.md"), shippedSkill());
1121
+ writeFileSync6(join9(primary, OWN_SKILL_MARKER), "Installed by skilllib. `skilllib agent-skill remove` removes it.\n");
1122
+ for (const link2 of links) {
1123
+ if (entryExists2(link2)) continue;
1124
+ mkdirSync6(join9(link2, ".."), { recursive: true });
1125
+ linkDir(primary, link2);
1126
+ }
1127
+ return { ok: true, dirs: [primary, ...links] };
1128
+ }
1129
+ function removeAgentSkill() {
1130
+ const all = agentSkillDirs(["claude-code", "codex"]);
1131
+ const removed = all.filter((d) => entryExists2(d) && (isOurs(d) || isDangling(d)));
1132
+ for (const d of removed) {
1133
+ if (lstatSync3(d).isSymbolicLink()) unlinkSync2(d);
1134
+ else rmSync3(d, { recursive: true, force: true });
1135
+ }
1136
+ return removed;
1137
+ }
1138
+ function refreshAgentSkill() {
1139
+ try {
1140
+ if (agentSkillState() === "outdated") installAgentSkill();
1141
+ } catch {
1068
1142
  }
1069
- return [...bySkill.values()].sort((a, b) => b.uses - a.uses);
1070
1143
  }
1071
1144
 
1072
- // src/health.ts
1073
- import { basename as basename5 } from "path";
1145
+ // 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";
1074
1152
 
1075
1153
  // src/git.ts
1076
1154
  import { execFileSync as execFileSync2 } from "child_process";
@@ -1080,8 +1158,8 @@ function git(root, args, input) {
1080
1158
  try {
1081
1159
  return execFileSync2("git", args, { cwd: root, encoding: "utf-8", input, stdio: ["pipe", "pipe", "ignore"], maxBuffer: 16 * 1024 * 1024 });
1082
1160
  } catch (err) {
1083
- const out = err.stdout;
1084
- return typeof out === "string" ? out : null;
1161
+ const out2 = err.stdout;
1162
+ return typeof out2 === "string" ? out2 : null;
1085
1163
  }
1086
1164
  }
1087
1165
  function gitInfo(root) {
@@ -1093,8 +1171,8 @@ function gitInfo(root) {
1093
1171
  const ignoredCache = /* @__PURE__ */ new Map();
1094
1172
  const isIgnored = (rel) => {
1095
1173
  if (!ignoredCache.has(rel)) {
1096
- const out = git(root, ["check-ignore", "--no-index", "--", rel]) ?? "";
1097
- ignoredCache.set(rel, out.trim().length > 0);
1174
+ const out2 = git(root, ["check-ignore", "--no-index", "--", rel]) ?? "";
1175
+ ignoredCache.set(rel, out2.trim().length > 0);
1098
1176
  }
1099
1177
  return ignoredCache.get(rel);
1100
1178
  };
@@ -1111,20 +1189,24 @@ function relativeTo(root, path) {
1111
1189
  return relative3(root, path).split("\\").join("/");
1112
1190
  }
1113
1191
 
1192
+ // src/health.ts
1193
+ import { lstatSync as lstatSync4 } from "fs";
1194
+ import { basename as basename6 } from "path";
1195
+
1114
1196
  // src/plugins.ts
1115
1197
  import { execFileSync as execFileSync3 } from "child_process";
1116
- import { existsSync as existsSync10, mkdirSync as mkdirSync6, readdirSync as readdirSync6, readFileSync as readFileSync7, statSync as statSync3, writeFileSync as writeFileSync6 } from "fs";
1198
+ import { existsSync as existsSync10, mkdirSync as mkdirSync7, readdirSync as readdirSync5, readFileSync as readFileSync8, statSync as statSync2, writeFileSync as writeFileSync7 } from "fs";
1117
1199
  import { basename as basename4, dirname as dirname3, join as join11 } from "path";
1118
1200
  function readJson2(path) {
1119
1201
  try {
1120
- return JSON.parse(readFileSync7(path, "utf-8"));
1202
+ return JSON.parse(readFileSync8(path, "utf-8"));
1121
1203
  } catch {
1122
1204
  return null;
1123
1205
  }
1124
1206
  }
1125
1207
  function nonEmptyDir(path) {
1126
1208
  try {
1127
- return readdirSync6(path).some((f) => !f.startsWith("."));
1209
+ return readdirSync5(path).some((f) => !f.startsWith("."));
1128
1210
  } catch {
1129
1211
  return false;
1130
1212
  }
@@ -1138,14 +1220,15 @@ function extrasOf(root) {
1138
1220
  ...nonEmptyDir(join11(root, "commands")) || manifest.commands ? ["commands"] : []
1139
1221
  ];
1140
1222
  }
1141
- function claudePlugins(machine) {
1223
+ function claudePlugins(machine, root) {
1142
1224
  const installed = readJson2(join11(claudeDir(), "plugins", "installed_plugins.json"))?.plugins ?? {};
1143
1225
  const byOrigin = /* @__PURE__ */ new Map();
1144
1226
  for (const m of machine) if (m.kind === "plugin") byOrigin.set(m.origin, [...byOrigin.get(m.origin) ?? [], m.path]);
1145
1227
  return [...byOrigin].map(([origin, skills]) => {
1146
1228
  const synced = origin.endsWith("@synced");
1147
1229
  const id = origin;
1148
- const entry = installed[origin]?.[0];
1230
+ const entries = installed[origin] ?? [];
1231
+ const entry = entries.find((e) => root && e.projectPath === root) ?? entries.find((e) => (e.scope ?? "user") === "user") ?? entries[0];
1149
1232
  return {
1150
1233
  id,
1151
1234
  scope: synced ? null : entry?.scope ?? "user",
@@ -1177,15 +1260,21 @@ function claude(args, cwd) {
1177
1260
  return { ok: false, reason: `${(e.stderr || e.stdout || e.message || "failed").trim().split("\n").slice(-1)[0]} (ran ${bin})` };
1178
1261
  }
1179
1262
  }
1180
- function turnOffSynced(id) {
1181
- const file = join11(claudeDir(), "settings.json");
1263
+ function setPluginIn(file, id, on) {
1182
1264
  const settings = existsSync10(file) ? readJson2(file) : {};
1183
1265
  if (!settings) return { ok: false, reason: `can't read ${file}` };
1184
1266
  const enabled = settings.enabledPlugins ?? {};
1185
- mkdirSync6(claudeDir(), { recursive: true });
1186
- writeFileSync6(file, JSON.stringify({ ...settings, enabledPlugins: { ...enabled, [id]: false } }, null, 2) + "\n");
1267
+ mkdirSync7(dirname3(file), { recursive: true });
1268
+ writeFileSync7(file, JSON.stringify({ ...settings, enabledPlugins: { ...enabled, [id]: on } }, null, 2) + "\n");
1187
1269
  return { ok: true };
1188
1270
  }
1271
+ function turnOffSynced(id) {
1272
+ return setPluginIn(join11(claudeDir(), "settings.json"), id, false);
1273
+ }
1274
+ 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}` };
1277
+ }
1189
1278
  function removedFile() {
1190
1279
  return join11(skilllibHome(), "plugin-backup.json");
1191
1280
  }
@@ -1206,8 +1295,8 @@ function removePlugin(plugin, how) {
1206
1295
  }
1207
1296
  function recordRemovedPlugin(id, scope) {
1208
1297
  const list2 = readJson2(removedFile()) ?? [];
1209
- mkdirSync6(skilllibHome(), { recursive: true });
1210
- writeFileSync6(removedFile(), JSON.stringify([...list2, { id, scope, at: (/* @__PURE__ */ new Date()).toISOString() }], null, 2) + "\n");
1298
+ mkdirSync7(skilllibHome(), { recursive: true });
1299
+ writeFileSync7(removedFile(), JSON.stringify([...list2, { id, scope, at: (/* @__PURE__ */ new Date()).toISOString() }], null, 2) + "\n");
1211
1300
  }
1212
1301
  function pluginBackups() {
1213
1302
  return (readJson2(removedFile()) ?? []).map((r) => ({
@@ -1221,24 +1310,24 @@ function pluginBackups() {
1221
1310
  function restorePlugin(backup) {
1222
1311
  const r = claude(["plugin", "install", backup.from, "--scope", backup.path]);
1223
1312
  if (!r.ok) return r;
1224
- writeFileSync6(removedFile(), JSON.stringify((readJson2(removedFile()) ?? []).filter((x) => x.id !== backup.from), null, 2) + "\n");
1313
+ writeFileSync7(removedFile(), JSON.stringify((readJson2(removedFile()) ?? []).filter((x) => x.id !== backup.from), null, 2) + "\n");
1225
1314
  return { ok: true, to: `Claude Code plugins (${backup.path})` };
1226
1315
  }
1227
1316
  function cursorPluginSkills() {
1228
1317
  const cache = join11(userHome(), ".cursor", "plugins", "cache");
1229
- const dirs = (p) => existsSync10(p) ? readdirSync6(p).filter((d) => !d.startsWith(".") && statSync3(join11(p, d)).isDirectory()) : [];
1318
+ const dirs = (p) => existsSync10(p) ? readdirSync5(p).filter((d) => !d.startsWith(".") && statSync2(join11(p, d)).isDirectory()) : [];
1230
1319
  return dirs(cache).flatMap(
1231
1320
  (marketplace) => dirs(join11(cache, marketplace)).flatMap((plugin) => {
1232
1321
  const versions = dirs(join11(cache, marketplace, plugin)).map((v) => join11(cache, marketplace, plugin, v));
1233
- const newest = versions.sort((a, b) => statSync3(b).mtimeMs - statSync3(a).mtimeMs)[0];
1322
+ const newest = versions.sort((a, b) => statSync2(b).mtimeMs - statSync2(a).mtimeMs)[0];
1234
1323
  return newest ? skillDirsIn(join11(newest, "skills")).map((path) => ({ name: basename4(path), plugin, path })) : [];
1235
1324
  })
1236
1325
  );
1237
1326
  }
1238
1327
 
1239
1328
  // src/tidy.ts
1240
- import { existsSync as existsSync11, mkdirSync as mkdirSync7, readdirSync as readdirSync7, unlinkSync as unlinkSync2 } from "fs";
1241
- import { dirname as dirname4, join as join12, sep as sep4 } from "path";
1329
+ import { existsSync as existsSync11, mkdirSync as mkdirSync8, readdirSync as readdirSync6, unlinkSync as unlinkSync3 } from "fs";
1330
+ import { basename as basename5, dirname as dirname4, join as join12, sep as sep4 } from "path";
1242
1331
  function entriesOf(dirs, name) {
1243
1332
  return dirs.flatMap((dir) => {
1244
1333
  const path = join12(dir, name);
@@ -1257,20 +1346,22 @@ function namesIn(dirs) {
1257
1346
  const names = /* @__PURE__ */ new Set();
1258
1347
  for (const dir of dirs) {
1259
1348
  if (!existsSync11(dir)) continue;
1260
- for (const name of readdirSync7(dir)) if (!name.startsWith(".") && name !== "synced") names.add(name);
1349
+ for (const name of readdirSync6(dir)) if (!name.startsWith(".") && name !== "synced") names.add(name);
1261
1350
  }
1262
1351
  return [...names].sort();
1263
1352
  }
1264
1353
  function runsFrom(id, dirsWithCopy, readsOf) {
1265
1354
  const reads = readsOf(id).filter((d) => dirsWithCopy.includes(d));
1266
- return reads.find((d) => d.endsWith(join12(".claude", "skills"))) ?? reads[0];
1355
+ if (id === "codex") return reads;
1356
+ const pick = (d) => CURSOR_PICKS.indexOf(basename5(dirname4(d)));
1357
+ return id === "cursor" ? [...reads].sort((a, b) => pick(a) - pick(b)).slice(0, 1) : reads.slice(0, 1);
1267
1358
  }
1268
1359
  function conflictOf(name, root, reals, enabled, readsOf) {
1269
1360
  const dirs = reals.map((r) => r.dir);
1270
1361
  return {
1271
1362
  name,
1272
1363
  root,
1273
- copies: reals.map((r) => ({ dir: r.dir, path: r.path, runs: enabled.filter((id) => runsFrom(id, dirs, readsOf) === r.dir) }))
1364
+ copies: reals.map((r) => ({ dir: r.dir, path: r.path, runs: enabled.filter((id) => runsFrom(id, dirs, readsOf).includes(r.dir)) }))
1274
1365
  };
1275
1366
  }
1276
1367
  function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {}) {
@@ -1370,10 +1461,10 @@ function applyTidy(plan, { git: git2 = "keep", info: info2 } = {}) {
1370
1461
  for (const step of plan.steps) {
1371
1462
  if (held.includes(step) || !entryExists(step.path) || "target" in step && !entryExists(step.target)) continue;
1372
1463
  if (step.kind === "link" || step.kind === "unlink") {
1373
- if (isLink(step.path)) unlinkSync2(step.path);
1464
+ if (isLink(step.path)) unlinkSync3(step.path);
1374
1465
  } else stash(step.path, "tidy-backup");
1375
1466
  if (step.kind === "link" || step.kind === "replace") {
1376
- mkdirSync7(dirname4(step.path), { recursive: true });
1467
+ mkdirSync8(dirname4(step.path), { recursive: true });
1377
1468
  linkDir(step.target, step.path);
1378
1469
  }
1379
1470
  applied.push(step);
@@ -1421,7 +1512,7 @@ function orThrow(r) {
1421
1512
  var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
1422
1513
  var agentNames = (ids) => ids.map((id) => HARNESSES.find((h) => h.id === id).name).join(", ");
1423
1514
  function conflictIssue(c) {
1424
- const where = c.root ? basename5(c.root) : "your global folders";
1515
+ const where = c.root ? basename6(c.root) : "your global folders";
1425
1516
  const winners = c.managed ? c.copies.filter((x) => x.dir === c.managed) : c.copies;
1426
1517
  return {
1427
1518
  id: `conflict:${c.root ?? "global"}:${c.name}`,
@@ -1444,9 +1535,10 @@ function applyAll(plans, git2) {
1444
1535
  const results = plans.map((p) => applyTidy(p, { git: git2 }));
1445
1536
  return { skills: results.filter((r) => r.applied.length).length, held: results.reduce((n, r) => n + r.held.length, 0) };
1446
1537
  }
1447
- function findIssues(projects2, machine, libraryNames, statusOf = projectStatus) {
1538
+ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus, loadedIn = (root) => skillsLoadedIn(root, machine), nestedOf = nestedSkills) {
1448
1539
  const issues = [];
1449
1540
  const statuses = new Map(projects2.map((root) => [root, statusOf(root)]));
1541
+ const nested = new Map(projects2.map((root) => [root, nestedOf(root)]));
1450
1542
  for (const skill of machine.filter((m) => m.broken)) {
1451
1543
  issues.push({
1452
1544
  id: `broken:${skill.name}`,
@@ -1474,13 +1566,42 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus)
1474
1566
  id: `dup:${name}`,
1475
1567
  severity: "problem",
1476
1568
  title: `${name}: loaded twice in ${harnesses.join(" and ")}`,
1477
- detail: `${copies.map((c) => `${c.kind} (${c.origin})`).join(" and ")}. Your skill wins: turn the ${vendor.map((c) => c.kind).join(" and ")} copy off at its source${vendor.some((c) => c.kind === "claude.ai") ? " (claude.ai \u2192 Settings \u2192 Skills)" : ""}.`
1569
+ detail: `${copies.map((c) => `${c.kind} (${c.origin})`).join(" and ")}. Your skill wins: turn the ${vendor.map((c) => c.kind).join(" and ")} copy off at its source${vendor.some((c) => c.kind === "claude.ai") ? " (claude.ai \u2192 Settings \u2192 Skills)" : ""}${vendor.some((c) => c.kind === "system") ? ` (whoever manages ${vendor.find((c) => c.kind === "system").origin})` : ""}.`
1478
1570
  });
1479
1571
  }
1480
- const loadedYours = /* @__PURE__ */ new Set([...loaded.filter((m) => m.movable).map((m) => m.name), ...[...statuses.values()].flat().map((s) => s.name)]);
1481
- const yours = /* @__PURE__ */ new Set([...libraryNames, ...loadedYours]);
1482
- for (const plugin of claudePlugins(machine)) {
1483
- const dupes = plugin.skills.map((p) => basename5(p)).filter((n) => yours.has(n));
1572
+ 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))];
1575
+ const globalYours = new Set(loaded.filter((m) => m.movable).map((m) => m.name));
1576
+ const installedYours = /* @__PURE__ */ new Set([...globalYours, ...[...statuses.values()].flat().map((s) => s.name)]);
1577
+ const repoChoices = (plugin, root, names) => {
1578
+ const where = basename6(root);
1579
+ const copies = statuses.get(root).filter((s) => names.includes(s.name));
1580
+ const allManaged = names.every((n) => copies.some((c) => c.name === n && c.managed));
1581
+ return [
1582
+ { label: `Turn the plugin ${plugin.id} off in ${where} only`, hint: "other repos keep it", run: () => orThrow(turnOffIn(plugin, root)) },
1583
+ // Only copies skilllib installed: Your skills keeps them. A repo's own skill is the team's, never removed from here.
1584
+ ...names.length && allManaged ? [
1585
+ {
1586
+ label: `Remove your ${copies.map((s) => s.name).join(", ")} from ${where}`,
1587
+ hint: "the plugin's copy stays; Your skills keeps yours",
1588
+ run: () => {
1589
+ for (const s of copies) {
1590
+ const r = removeSkill(root, s.name);
1591
+ if (r.action !== "removed") throw new Error(`${s.name}: ${r.reason}`);
1592
+ }
1593
+ return `${copies.map((s) => s.name).join(", ")} removed from ${where}; the plugin's copy stays`;
1594
+ }
1595
+ }
1596
+ ] : []
1597
+ ];
1598
+ };
1599
+ const machinePlugins = claudePlugins(machine);
1600
+ for (const plugin of machinePlugins) {
1601
+ 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)]);
1604
+ const dupes = names.filter((n) => libraryNames.has(n) || loadedYours.has(n));
1484
1605
  if (!dupes.length) continue;
1485
1606
  const others = plugin.skills.length - dupes.length;
1486
1607
  const pluginName = plugin.id.split("@")[0];
@@ -1490,24 +1611,44 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus)
1490
1611
  run: () => orThrow(removePlugin(plugin, "delete"))
1491
1612
  };
1492
1613
  const off = { label: `Turn the plugin ${plugin.id} off`, hint: "stays installed", run: () => orThrow(removePlugin(plugin, "off")) };
1493
- const notLoaded = dupes.filter((n) => !loadedYours.has(n));
1614
+ const bothLoad = dupes.filter((n) => loadedYours.has(n));
1615
+ const notInstalled = dupes.filter((n) => !installedYours.has(n));
1494
1616
  issues.push({
1495
1617
  id: `plugin:${plugin.id}`,
1496
- severity: notLoaded.length === dupes.length ? "suggestion" : "problem",
1618
+ severity: bothLoad.length ? "problem" : "suggestion",
1497
1619
  title: `${dupes.join(", ")}: also in the Claude Code plugin ${plugin.id}`,
1498
1620
  detail: [
1499
- notLoaded.length < dupes.length ? `Claude Code loads both (the plugin's as /${pluginName}:${dupes[0]}). Your skill wins.` : "",
1500
- notLoaded.length ? `Your ${notLoaded.join(", ")} ${notLoaded.length === 1 ? "is" : "are"} only in Your skills, not installed anywhere: once the plugin is gone, add ${notLoaded.length === 1 ? "it" : "them"} where you need ${notLoaded.length === 1 ? "it" : "them"}.` : "",
1621
+ bothLoad.length ? `Claude Code loads both (the plugin's as /${pluginName}:${bothLoad[0]}). Your skill wins.` : "",
1622
+ 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"}.` : "",
1501
1623
  others ? `The plugin's other ${plural(others, "skill")} are copied into Your skills first, so nothing is lost.` : "",
1502
1624
  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"}.` : "",
1503
- plugin.synced ? "It's synced from claude.ai, so it can only be turned off here." : ""
1625
+ plugin.synced ? "It's synced from claude.ai, so it can only be turned off here." : "",
1626
+ inRepos.length ? `Or keep it and turn it off only in ${inRepos.map(([root]) => basename6(root)).join(", ")}.` : ""
1504
1627
  ].filter(Boolean).join(" "),
1505
- choices: plugin.synced ? [off] : plugin.extras.length ? [off, remove2] : [remove2, off]
1628
+ choices: [...plugin.synced ? [off] : plugin.extras.length ? [off, remove2] : [remove2, off], ...inRepos.flatMap(([root, d]) => repoChoices(plugin, root, d))]
1506
1629
  });
1507
1630
  }
1631
+ const machinePluginSkills = new Set(machinePlugins.flatMap((p) => p.skills));
1632
+ for (const root of projects2) {
1633
+ const where = basename6(root);
1634
+ const repoOnly = loadedHere.get(root).filter((m) => m.kind === "plugin" && !machinePluginSkills.has(m.path));
1635
+ for (const plugin of claudePlugins(repoOnly, root)) {
1636
+ const names = plugin.skills.map((p) => basename6(p));
1637
+ const inRepo = namesIn2(root).filter((n) => names.includes(n));
1638
+ const dupes = names.filter((n) => inRepo.includes(n) || globalYours.has(n));
1639
+ if (!dupes.length) continue;
1640
+ issues.push({
1641
+ id: `plugin:${root}:${plugin.id}`,
1642
+ severity: "problem",
1643
+ title: `${dupes.join(", ")}: also in the Claude Code plugin ${plugin.id}, on in ${where}`,
1644
+ detail: `${where}'s Claude Code settings turn the plugin on, so Claude Code loads both there (the plugin's as /${plugin.id.split("@")[0]}:${dupes[0]}). Your skill wins.`,
1645
+ choices: repoChoices(plugin, root, inRepo)
1646
+ });
1647
+ }
1648
+ }
1508
1649
  if (enabledHarnesses().includes("cursor")) {
1509
1650
  const byPlugin = /* @__PURE__ */ new Map();
1510
- for (const s of cursorPluginSkills().filter((x) => loadedYours.has(x.name))) byPlugin.set(s.plugin, [...byPlugin.get(s.plugin) ?? [], s.name]);
1651
+ for (const s of cursorPluginSkills().filter((x) => installedYours.has(x.name))) byPlugin.set(s.plugin, [...byPlugin.get(s.plugin) ?? [], s.name]);
1511
1652
  for (const [plugin, names] of byPlugin) {
1512
1653
  issues.push({
1513
1654
  id: `cursor-plugin:${plugin}`,
@@ -1532,10 +1673,46 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus)
1532
1673
  }
1533
1674
  issues.push(...global.conflicts.map(conflictIssue));
1534
1675
  const kept = keptGlobal();
1676
+ const unreviewed = [...new Set(loaded.filter((m) => m.movable && !kept.has(m.name)).map((m) => m.name))];
1677
+ if (unreviewed.length) {
1678
+ issues.push({
1679
+ id: "review:global",
1680
+ severity: "suggestion",
1681
+ 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>).`
1683
+ });
1684
+ }
1685
+ const enabled = enabledHarnesses();
1686
+ const seenBy = /* @__PURE__ */ new Map();
1687
+ for (const m of loaded) seenBy.set(m.name, /* @__PURE__ */ new Set([...seenBy.get(m.name) ?? [], ...m.harnesses]));
1688
+ const blindOf = (m) => enabled.filter((id) => !seenBy.get(m.name).has(id));
1689
+ const blindGlobal = loaded.filter((m) => m.movable && m.harnesses.length && blindOf(m).length);
1690
+ for (const onPurpose of [true, false]) {
1691
+ const skills = blindGlobal.filter((m) => kept.has(m.name) === onPurpose);
1692
+ if (!skills.length) continue;
1693
+ const missing = [...new Set(skills.flatMap(blindOf))];
1694
+ const link2 = {
1695
+ label: `Link ${skills.length === 1 ? "it" : "them"} for ${agentNames(missing)}`,
1696
+ hint: "links only; nothing is copied or moved",
1697
+ run: () => {
1698
+ const results = skills.map((m) => ({ m, ...linkGlobal(m.path, blindOf(m)) }));
1699
+ const skipped = results.flatMap((r) => r.skipped);
1700
+ 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
+ }
1702
+ };
1703
+ issues.push({
1704
+ id: `usable:global${onPurpose ? ":kept" : ""}`,
1705
+ severity: "suggestion",
1706
+ title: `${plural(skills.length, "global skill")}${onPurpose ? " you keep global" : ""} not usable by ${agentNames(missing)}`,
1707
+ detail: `${skills.map((m) => `${m.name} (only ${agentNames(m.harnesses)})`).join(", ")}. ${onPurpose ? "A link in each agent's global folder makes the one real copy reach every agent you use." : `Link ${skills.length === 1 ? "it" : "them"} for every agent, or move ${skills.length === 1 ? "it" : "them"} to the repos that need ${skills.length === 1 ? "it" : "them"} (skilllib \u2192 Global), which works for every agent too.`}`,
1708
+ ...onPurpose ? { fix: link2 } : { choices: [link2] }
1709
+ });
1710
+ }
1535
1711
  const alsoGlobal = /* @__PURE__ */ new Map();
1536
- const globalNames = new Set(loaded.filter((m) => m.kind !== "plugin").map((m) => m.name));
1712
+ const globalCopies = loaded.filter((m) => m.kind !== "plugin");
1713
+ const alsoLoadedGlobally = (s) => globalCopies.some((m) => m.name === s.name && m.harnesses.some((id) => s.visibility.some((v) => v.id === id && v.paths > 0)));
1537
1714
  for (const root of projects2) {
1538
- const where = basename5(root);
1715
+ const where = basename6(root);
1539
1716
  const status2 = statuses.get(root);
1540
1717
  const behind = status2.filter((s) => s.state === "update available" || s.state === "folder missing");
1541
1718
  if (behind.length > 0) {
@@ -1627,27 +1804,67 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus)
1627
1804
  choices: [{ label: "Track it", hint: `adds it to ${where}'s skilllib.json`, run: () => `${addSkill(root, s.name).name} tracked in ${where}` }]
1628
1805
  });
1629
1806
  }
1630
- if (globalNames.has(s.name) && s.managed) alsoGlobal.set(s.name, [...alsoGlobal.get(s.name) ?? [], root]);
1807
+ if (s.state === "edited locally" || s.state === "edited locally, update available") {
1808
+ const newer = s.state !== "edited locally";
1809
+ const pinned = s.version ? `v${s.version}` : "the library version";
1810
+ issues.push({
1811
+ id: `edited:${root}:${s.name}`,
1812
+ severity: "suggestion",
1813
+ title: `${s.name} in ${where} has local edits`,
1814
+ detail: `It no longer matches ${pinned} from Your skills${newer ? `, and v${s.latest} is out` : ""}. Save the edits as a new version so your other repos can get them, or discard them. Keeping them is fine too: skilllib never overwrites edits.`,
1815
+ choices: [
1816
+ {
1817
+ label: "Save as a new version in Your skills",
1818
+ hint: newer ? `becomes v${s.latest + 1}; v${s.latest}'s changes aren't merged in` : `becomes v${(s.latest ?? 0) + 1}; ${where} uses it`,
1819
+ run: () => {
1820
+ importSkill(s.path, { force: true });
1821
+ const c = addSkill(root, s.name);
1822
+ if (c.action === "skipped") throw new Error(`${s.name}: ${c.reason}`);
1823
+ return `${s.name}: saved as v${c.to}; ${where} uses it`;
1824
+ }
1825
+ },
1826
+ {
1827
+ label: "Discard the edits",
1828
+ hint: `back to ${pinned}; the edited copy is restorable from Health`,
1829
+ run: () => {
1830
+ const c = discardEdits(root, s.name);
1831
+ if (c.action === "skipped") throw new Error(`${s.name}: ${c.reason}`);
1832
+ return `${s.name}: edits discarded, back to v${c.to}`;
1833
+ }
1834
+ }
1835
+ ]
1836
+ });
1837
+ }
1838
+ if (alsoLoadedGlobally(s)) alsoGlobal.set(s.name, [...alsoGlobal.get(s.name) ?? [], { root, managed: s.managed }]);
1631
1839
  }
1632
1840
  }
1633
- for (const [name, roots] of alsoGlobal) {
1634
- const wheres = roots.map((r) => basename5(r)).join(", ");
1841
+ for (const root of projects2) {
1842
+ for (const n of nested.get(root)) {
1843
+ const copies = alsoGlobal.get(n.name) ?? [];
1844
+ if (alsoLoadedGlobally(n) && !copies.some((c) => c.root === root)) alsoGlobal.set(n.name, [...copies, { root, managed: false }]);
1845
+ }
1846
+ }
1847
+ for (const [name, copies] of alsoGlobal) {
1848
+ const wheres = copies.map((c) => basename6(c.root)).join(", ");
1849
+ const removable = copies.filter((c) => c.managed).map((c) => c.root);
1850
+ const repoOwn = copies.filter((c) => !c.managed).map((c) => basename6(c.root));
1635
1851
  if (kept.has(name)) {
1852
+ const removeWheres = removable.map((r) => basename6(r)).join(", ");
1636
1853
  issues.push({
1637
1854
  id: `twice:${name}`,
1638
1855
  severity: "suggestion",
1639
1856
  title: `${name}: in ${wheres}, and you keep it global`,
1640
- detail: "The global copy already loads there, so the project copies are extra. Claude Code runs the global one anyway.",
1641
- choices: [
1857
+ detail: `The global copy already loads there, so the project copies are extra. Claude Code runs the global one anyway.${repoOwn.length ? ` ${repoOwn.join(", ")} ${repoOwn.length === 1 ? "has its own copy" : "have their own copies"}: remove ${repoOwn.length === 1 ? "it" : "them"} in the repo, or stop keeping ${name} global.` : ""}`,
1858
+ choices: removable.length ? [
1642
1859
  {
1643
- label: `Remove the copies in ${wheres}`,
1860
+ label: `Remove the copies in ${removeWheres}`,
1644
1861
  hint: "the global copy stays",
1645
1862
  run: () => {
1646
- const removed = roots.filter((root) => removeSkill(root, name).action === "removed").map((r) => basename5(r));
1863
+ const removed = removable.filter((root) => removeSkill(root, name).action === "removed").map((r) => basename6(r));
1647
1864
  return `${name}: removed from ${removed.join(", ") || "no project"}; the global copy stays`;
1648
1865
  }
1649
1866
  }
1650
- ]
1867
+ ] : void 0
1651
1868
  });
1652
1869
  continue;
1653
1870
  }
@@ -1656,7 +1873,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus)
1656
1873
  id: `twice:${name}`,
1657
1874
  severity: "suggestion",
1658
1875
  title: `${name}: in ${wheres} and also loaded globally`,
1659
- detail: "The project copies are enough; the global one loads it in every other project too. Or keep it global on purpose (Global \u2192 Enter).",
1876
+ detail: `${repoOwn.length === copies.length ? `Agents there load both the repo's copy and the global one.` : "The project copies are enough; the global one loads it in every other project too."} ${movable ? "Stop loading it globally, or keep it global on purpose (Global \u2192 Enter)." : `The global copy comes from ${machine.find((m) => m.name === name && !m.movable)?.origin ?? "a vendor"}: turn it off there.`}`,
1660
1877
  choices: movable ? [
1661
1878
  {
1662
1879
  label: "Stop loading it globally",
@@ -1671,9 +1888,305 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus)
1671
1888
  ] : void 0
1672
1889
  });
1673
1890
  }
1891
+ const agentSkill2 = agentSkillState();
1892
+ if (agentSkill2 !== "installed") {
1893
+ const install = {
1894
+ label: agentSkill2 === "missing" ? "Install the skilllib skill for your agents" : "Update the skilllib skill",
1895
+ hint: "one global skill, in ~/.claude/skills (and ~/.agents/skills)",
1896
+ run: () => {
1897
+ const r = installAgentSkill();
1898
+ if (!r.ok) throw new Error(`skilllib skill: ${r.reason}`);
1899
+ return `Your agents can now use skilllib (${plural(r.dirs.length, "folder")})`;
1900
+ }
1901
+ };
1902
+ issues.push({
1903
+ id: "agent-skill",
1904
+ severity: "suggestion",
1905
+ title: agentSkill2 === "missing" ? "Your agents don't know about skilllib" : "The skilllib skill for your agents is out of date",
1906
+ detail: "The skilllib skill lets you ask your agents which skills they can use here, where each comes from, and which of your skills a repo should add.",
1907
+ ...agentSkill2 === "missing" ? { choices: [install] } : { fix: install }
1908
+ });
1909
+ }
1910
+ return issues;
1911
+ }
1912
+ function addedAt(path) {
1913
+ try {
1914
+ const st = lstatSync4(path);
1915
+ return st.birthtimeMs > 0 ? st.birthtimeMs : st.ctimeMs;
1916
+ } catch {
1917
+ return Date.now();
1918
+ }
1919
+ }
1920
+ function usageIssues(projects2, machine, libraryNames, uses, statusOf = projectStatus) {
1921
+ if (!enabledHarnesses().includes("claude-code")) return [];
1922
+ const issues = [];
1923
+ const since = Date.now() - uses.days * 24 * 60 * 60 * 1e3;
1924
+ for (const root of projects2) {
1925
+ const where = basename6(root);
1926
+ const unused = statusOf(root).filter((s) => s.state !== "folder missing" && uses.inProject(root, s.name) === 0 && addedAt(s.path) <= since);
1927
+ if (!unused.length) continue;
1928
+ const removable = unused.filter((s) => s.managed);
1929
+ issues.push({
1930
+ id: `unused:${root}`,
1931
+ severity: "suggestion",
1932
+ title: `${where}: ${plural(unused.length, "skill")} unused in ${uses.days} days`,
1933
+ detail: `${unused.map((s) => s.name).join(", ")}: no Claude Code session in ${where} used ${unused.length === 1 ? "it" : "them"}. Each one's name and description still takes context in every session there.${unused.length > removable.length ? " The repo's own skills are the team's: remove those in the repo if nobody needs them." : ""}`,
1934
+ choices: removable.length ? [
1935
+ {
1936
+ label: `Remove ${removable.length === unused.length ? "them" : `the ${plural(removable.length, "skill")} skilllib installed`} from ${where}`,
1937
+ hint: "Your skills keeps them; edited ones stay",
1938
+ run: () => {
1939
+ const changes = removable.map((s) => removeSkill(root, s.name));
1940
+ const removed = changes.filter((c) => c.action === "removed").map((c) => c.name);
1941
+ const kept = changes.filter((c) => c.action !== "removed").map((c) => c.name);
1942
+ return `${where}: removed ${removed.join(", ") || "nothing"}${kept.length ? `; kept ${kept.join(", ")} (local edits)` : ""}`;
1943
+ }
1944
+ }
1945
+ ] : void 0
1946
+ });
1947
+ }
1948
+ const installed = /* @__PURE__ */ new Set([...projects2.flatMap((root) => statusOf(root).map((s) => s.name)), ...machine.filter((m) => !m.broken).map((m) => m.name)]);
1949
+ const idle = [...libraryNames].filter((name) => {
1950
+ const first = versionHistory(name)[0];
1951
+ return !installed.has(name) && uses.total(name) === 0 && (first ? Date.parse(first.date) : addedAt(librarySkillDir(name))) <= since;
1952
+ });
1953
+ if (idle.length) {
1954
+ issues.push({
1955
+ id: "unused:library",
1956
+ severity: "suggestion",
1957
+ title: `${plural(idle.length, "skill")} in Your skills: in no repo, unused in ${uses.days} days`,
1958
+ detail: `${idle.sort().join(", ")}. They load nowhere, so they take no context: add the ones you want to a repo, or delete the ones you've outgrown.`,
1959
+ choices: [
1960
+ {
1961
+ label: `Delete ${idle.length === 1 ? "it" : `all ${idle.length}`} from Your skills`,
1962
+ hint: "restorable from Health",
1963
+ run: () => {
1964
+ const done = idle.map((name) => [name, deleteLibrarySkill(name)]);
1965
+ const failed = done.filter(([, r]) => !r.ok).map(([name]) => name);
1966
+ return `Deleted ${plural(done.length - failed.length, "skill")} from Your skills${failed.length ? `; kept ${failed.join(", ")}` : ""}`;
1967
+ }
1968
+ }
1969
+ ]
1970
+ });
1971
+ }
1674
1972
  return issues;
1675
1973
  }
1676
1974
 
1975
+ // src/here.ts
1976
+ function groupBy(items) {
1977
+ const groups = /* @__PURE__ */ new Map();
1978
+ for (const [key, name, make] of items) {
1979
+ const group = groups.get(key) ?? make();
1980
+ group.skills.push(name);
1981
+ groups.set(key, group);
1982
+ }
1983
+ return [...groups.values()];
1984
+ }
1985
+ var sameSet = (a, b) => a.length === b.length && a.every((x) => b.includes(x));
1986
+ function globalFrom(kind, origin, path) {
1987
+ if (kind === "global") return tildify(path.replace(/[\\/][^\\/]+$/, "")).replace(/\\/g, "/");
1988
+ return origin;
1989
+ }
1990
+ var SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "target", "vendor", "coverage"]);
1991
+ function topLevel(root) {
1992
+ try {
1993
+ return readdirSync7(root, { withFileTypes: true }).filter((d) => !d.name.startsWith(".") && !SKIP.has(d.name)).map((d) => d.isDirectory() ? `${d.name}/` : d.name).sort().slice(0, 60);
1994
+ } catch {
1995
+ return [];
1996
+ }
1997
+ }
1998
+ function usableHere(root, uses = /* @__PURE__ */ new Map()) {
1999
+ 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) => {
2005
+ const loadedBy = s.visibility.filter((v) => v.paths > 0).map((v) => v.id);
2006
+ const behind = s.managed && s.latest !== null && s.version !== null && s.latest > s.version;
2007
+ const attrs = {
2008
+ source: s.managed ? "library" : s.state.startsWith("repo skill") ? "repo" : "local",
2009
+ ...s.location !== ".claude/skills" && { dir: s.location },
2010
+ ...s.version !== null && { version: s.version },
2011
+ ...s.state !== "ok" && s.state !== "local only" && !s.state.startsWith("repo skill") && { state: s.state },
2012
+ ...behind && { latest: s.latest },
2013
+ ...git2 && { git: git2.of(relativeTo(root, s.path)) },
2014
+ ...!sameSet(loadedBy, agents) && { agents: loadedBy }
2015
+ };
2016
+ return [JSON.stringify(attrs), s.name, () => ({ ...attrs, skills: [] })];
2017
+ })
2018
+ );
2019
+ const machine = machineSkills(agents);
2020
+ const global = groupBy(
2021
+ machine.filter((m) => !m.broken && m.harnesses.length > 0).map((m) => {
2022
+ const from = globalFrom(m.kind, m.origin, m.path);
2023
+ return [`${m.kind}\0${from}\0${m.harnesses.join()}`, m.name, () => ({ source: m.kind, from, agents: m.harnesses, skills: [] })];
2024
+ })
2025
+ );
2026
+ const issues = /* @__PURE__ */ new Map();
2027
+ for (const i of findIssues(inRepo ? [root] : [], machine, new Set(librarySkills().map((l) => l.name)))) {
2028
+ const choices = i.choices?.map((c) => c.label);
2029
+ const key = `${i.detail}\0${i.fix?.label ?? ""}\0${choices?.join() ?? ""}`;
2030
+ const issue = issues.get(key) ?? { problems: [], detail: i.detail, ...i.fix && { fix: i.fix.label }, ...choices && { choices } };
2031
+ issue.problems.push(i.title);
2032
+ issues.set(key, issue);
2033
+ }
2034
+ const usesHere = Object.fromEntries([...uses].filter(([, n]) => n > 0));
2035
+ return {
2036
+ project: tildify(root),
2037
+ files: topLevel(root),
2038
+ agents,
2039
+ skills,
2040
+ global,
2041
+ issues: [...issues.values()],
2042
+ ...Object.keys(usesHere).length > 0 && { usesHere }
2043
+ };
2044
+ }
2045
+ function firstSentence(description) {
2046
+ const line = description.replace(/\s+/g, " ").trim();
2047
+ const sentence = line.match(/^(.{40,}?[.!?])(?:\s|$)/)?.[1] ?? line;
2048
+ return sentence.length > 200 ? sentence.slice(0, 199).trimEnd() + "\u2026" : sentence;
2049
+ }
2050
+ function libraryFor(currentProject2) {
2051
+ 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;
2053
+ const all = librarySkills();
2054
+ const describe = (s) => {
2055
+ const projects2 = manifests.filter((m) => s.name in m.skills).map((m) => m.name);
2056
+ return { name: s.name, description: firstSentence(s.description), ...projects2.length > 0 && { projects: projects2 } };
2057
+ };
2058
+ if (!here) return { skills: all.map(describe) };
2059
+ return { inThisProject: all.filter((s) => here.has(s.name)).map((s) => s.name), skills: all.filter((s) => !here.has(s.name)).map(describe) };
2060
+ }
2061
+
2062
+ // src/usage.ts
2063
+ import { createReadStream, existsSync as existsSync13, readdirSync as readdirSync8, statSync as statSync3 } from "fs";
2064
+ import { join as join14 } from "path";
2065
+ import { createInterface } from "readline";
2066
+ var BUILTIN_COMMANDS = /* @__PURE__ */ new Set([
2067
+ "add-dir",
2068
+ "agents",
2069
+ "bug",
2070
+ "clear",
2071
+ "compact",
2072
+ "config",
2073
+ "context",
2074
+ "cost",
2075
+ "doctor",
2076
+ "exit",
2077
+ "export",
2078
+ "fast",
2079
+ "feedback",
2080
+ "goal",
2081
+ "help",
2082
+ "hooks",
2083
+ "ide",
2084
+ "init",
2085
+ "install-github-app",
2086
+ "login",
2087
+ "logout",
2088
+ "mcp",
2089
+ "memory",
2090
+ "model",
2091
+ "output-style",
2092
+ "permissions",
2093
+ "plugin",
2094
+ "pr-comments",
2095
+ "privacy-settings",
2096
+ "release-notes",
2097
+ "resume",
2098
+ "rewind",
2099
+ "status",
2100
+ "statusline",
2101
+ "terminal-setup",
2102
+ "todos",
2103
+ "upgrade",
2104
+ "usage",
2105
+ "vim"
2106
+ ]);
2107
+ function skillNameFromPath(filePath) {
2108
+ const match = filePath.replace(/\\/g, "/").match(/\/\.(?:claude|agents)\/skills\/([^/]+)\//);
2109
+ return match ? match[1] : null;
2110
+ }
2111
+ function usesFromTranscriptLine(line) {
2112
+ if (!line.includes('"Skill"') && !line.includes("<command-name>") && !line.includes("/skills/")) return [];
2113
+ let record;
2114
+ try {
2115
+ record = JSON.parse(line);
2116
+ } catch {
2117
+ return [];
2118
+ }
2119
+ const cwd = record.cwd ?? "";
2120
+ const at = record.timestamp ?? "";
2121
+ const content = record.message?.content;
2122
+ if (record.type === "assistant" && Array.isArray(content)) {
2123
+ return content.flatMap((block) => {
2124
+ if (!block || typeof block !== "object" || block.type !== "tool_use") return [];
2125
+ const key = String(block.id);
2126
+ if (block.name === "Skill" && typeof block.input?.skill === "string") {
2127
+ return [{ skill: block.input.skill, source: "invoked", cwd, at, key }];
2128
+ }
2129
+ if (block.name === "Read" && typeof block.input?.file_path === "string") {
2130
+ const skill = skillNameFromPath(block.input.file_path);
2131
+ return skill ? [{ skill, source: "file read", cwd, at, key }] : [];
2132
+ }
2133
+ return [];
2134
+ });
2135
+ }
2136
+ if (record.type === "user" && typeof content === "string") {
2137
+ const match = content.match(/<command-name>\/?([\w.:-]+)<\/command-name>/);
2138
+ if (!match || BUILTIN_COMMANDS.has(match[1])) return [];
2139
+ return [{ skill: match[1], source: "slash", cwd, at, key: record.uuid ?? `${at}:${match[1]}` }];
2140
+ }
2141
+ return [];
2142
+ }
2143
+ function transcriptFiles(sinceMs) {
2144
+ const base = join14(claudeDir(), "projects");
2145
+ if (!existsSync13(base)) return [];
2146
+ 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);
2150
+ });
2151
+ }
2152
+ async function scanUsage(days) {
2153
+ const sinceMs = Date.now() - days * 24 * 60 * 60 * 1e3;
2154
+ const seen = /* @__PURE__ */ new Map();
2155
+ for (const file of transcriptFiles(sinceMs)) {
2156
+ const lines = createInterface({ input: createReadStream(file), crlfDelay: Infinity });
2157
+ for await (const line of lines) {
2158
+ for (const use of usesFromTranscriptLine(line)) {
2159
+ if (Date.parse(use.at) >= sinceMs) seen.set(use.key, use);
2160
+ }
2161
+ }
2162
+ }
2163
+ return [...seen.values()];
2164
+ }
2165
+ function summarize(uses, projectOf = () => null) {
2166
+ const bySkill = /* @__PURE__ */ new Map();
2167
+ for (const use of uses) {
2168
+ const skill = use.skill.split(":").pop();
2169
+ const summary = bySkill.get(skill) ?? { skill, uses: 0, lastUsed: "", projects: /* @__PURE__ */ new Set() };
2170
+ summary.uses += 1;
2171
+ if (use.at > summary.lastUsed) summary.lastUsed = use.at;
2172
+ const project = projectOf(use.cwd);
2173
+ if (project) summary.projects.add(project);
2174
+ bySkill.set(skill, summary);
2175
+ }
2176
+ return [...bySkill.values()].sort((a, b) => b.uses - a.uses);
2177
+ }
2178
+ function usesByProject(uses, projectOf) {
2179
+ const byProject = /* @__PURE__ */ new Map();
2180
+ for (const use of uses) {
2181
+ const root = projectOf(use.cwd);
2182
+ if (!root) continue;
2183
+ const counts = byProject.get(root) ?? /* @__PURE__ */ new Map();
2184
+ counts.set(use.skill, (counts.get(use.skill) ?? 0) + 1);
2185
+ byProject.set(root, counts);
2186
+ }
2187
+ return byProject;
2188
+ }
2189
+
1677
2190
  // src/commands.ts
1678
2191
  var USAGE_DAYS = 30;
1679
2192
  function parseArgs(argv) {
@@ -1687,6 +2200,7 @@ function parseArgs(argv) {
1687
2200
  global: argv.includes("--global"),
1688
2201
  fix: argv.includes("--fix"),
1689
2202
  allowTracked: argv.includes("--allow-tracked"),
2203
+ json: argv.includes("--json"),
1690
2204
  days: Number.isFinite(days) && days > 0 ? days : USAGE_DAYS,
1691
2205
  dryRun: argv.includes("--dry-run"),
1692
2206
  allowGit: argv.includes("--allow-git"),
@@ -1699,7 +2213,15 @@ function colorState(state) {
1699
2213
  if (state === "local only" || state === "from npx skills") return dim(state);
1700
2214
  return yellow(state);
1701
2215
  }
1702
- function printChanges(changes) {
2216
+ function withDirs(root, changes) {
2217
+ const { skills } = readManifest(root);
2218
+ return changes.map((c) => {
2219
+ const dep = c.action === "installed" || c.action === "updated" ? skills[c.name] : void 0;
2220
+ return dep ? { ...c, dirs: [dep.dir ?? ".claude/skills", ...dep.links ?? []] } : c;
2221
+ });
2222
+ }
2223
+ function printChanges(root, changes, args) {
2224
+ if (args.json) return json(withDirs(root, changes));
1703
2225
  for (const c of changes) {
1704
2226
  if (c.blocked?.length) {
1705
2227
  warn(`${c.name}: git tracks ${c.blocked.join(", ")} here, so no link was added there. Re-run with --allow-tracked to add it.`);
@@ -1728,27 +2250,51 @@ async function usageBySkill(days, onlyProject) {
1728
2250
  const scoped = onlyProject ? uses.filter((u) => projectOf(u.cwd) === onlyProject) : uses;
1729
2251
  return new Map(summarize(scoped, projectOf).map((s) => [s.skill, s]));
1730
2252
  }
2253
+ function currentProject() {
2254
+ const root = findProjectRoot();
2255
+ return isProjectCandidate(root) && (existsSync14(join15(root, ".git")) || existsSync14(join15(root, "skilllib.json"))) ? root : null;
2256
+ }
1731
2257
  function lastUsed(summary) {
1732
2258
  return summary ? summary.lastUsed.slice(0, 10) : "-";
1733
2259
  }
1734
2260
  async function status(args) {
1735
2261
  const root = findProjectRoot();
2262
+ 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]);
2266
+ return;
2267
+ }
1736
2268
  const skills = projectStatus(root);
1737
- info(`${basename6(root)} ${dim(tildify(root))}
2269
+ const nested = nestedSkills(root);
2270
+ info(`${basename8(root)} ${dim(tildify(root))}
1738
2271
  `);
1739
- if (skills.length === 0) {
2272
+ if (skills.length === 0 && nested.length === 0) {
1740
2273
  info("No skills in this project yet. Add one from your library with: skilllib add <name>");
1741
2274
  return;
1742
2275
  }
1743
2276
  const usage2 = await usageBySkill(args.days, root);
1744
- table(
1745
- skills.map((s) => ({
1746
- Skill: s.name,
1747
- Status: s.source ? dim(`npx skills: ${s.source}`) : colorState(s.state),
1748
- [`Uses (${args.days}d)`]: String(usage2.get(s.name)?.uses ?? 0),
1749
- "Last used": lastUsed(usage2.get(s.name))
1750
- }))
1751
- );
2277
+ if (skills.length) {
2278
+ table(
2279
+ skills.map((s) => ({
2280
+ Skill: s.name,
2281
+ Status: s.source ? dim(`npx skills: ${s.source}`) : colorState(s.state),
2282
+ [`Uses (${args.days}d)`]: String(usage2.get(s.name)?.uses ?? 0),
2283
+ "Last used": lastUsed(usage2.get(s.name))
2284
+ }))
2285
+ );
2286
+ }
2287
+ if (nested.length) {
2288
+ info(`${skills.length ? "\n" : ""}In subfolders ${dim("(Claude Code loads .claude/skills when you work there; Codex loads .agents/skills when started there)")}`);
2289
+ table(
2290
+ nested.map((s) => ({
2291
+ Skill: s.name,
2292
+ Folder: s.location,
2293
+ Status: colorState(s.state),
2294
+ [`Uses (${args.days}d)`]: String(usage2.get(s.name)?.uses ?? 0)
2295
+ }))
2296
+ );
2297
+ }
1752
2298
  const states = new Set(skills.map((s) => s.state));
1753
2299
  const hints = [];
1754
2300
  if (states.has("update available")) hints.push("`skilllib update` to install the newest versions");
@@ -1762,6 +2308,9 @@ async function status(args) {
1762
2308
  rememberProjects([root]);
1763
2309
  }
1764
2310
  async function list(args) {
2311
+ if (args.json) {
2312
+ return json(libraryFor(currentProject()));
2313
+ }
1765
2314
  const skills = librarySkills();
1766
2315
  if (skills.length === 0) {
1767
2316
  info(`Your library (${tildify(libraryDir())}) is empty.`);
@@ -1770,7 +2319,7 @@ async function list(args) {
1770
2319
  const installs = /* @__PURE__ */ new Map();
1771
2320
  for (const project of visibleProjects()) {
1772
2321
  for (const name of Object.keys(readManifest(project).skills)) {
1773
- installs.set(name, [...installs.get(name) ?? [], basename6(project)]);
2322
+ installs.set(name, [...installs.get(name) ?? [], basename8(project)]);
1774
2323
  }
1775
2324
  }
1776
2325
  const usage2 = await usageBySkill(args.days);
@@ -1800,8 +2349,17 @@ async function list(args) {
1800
2349
  );
1801
2350
  }
1802
2351
  }
1803
- function projects() {
2352
+ function projects(args) {
1804
2353
  const roots = visibleProjects();
2354
+ if (args.json) {
2355
+ return json(
2356
+ roots.map((root) => ({
2357
+ name: basename8(root),
2358
+ path: root,
2359
+ skills: projectStatus(root).map((s) => ({ name: s.name, managed: s.managed, state: s.state, version: s.version, latest: s.latest }))
2360
+ }))
2361
+ );
2362
+ }
1805
2363
  if (roots.length === 0) {
1806
2364
  info("No known projects. Run `skilllib scan <dir>` to find them, or `skilllib add` inside one.");
1807
2365
  return;
@@ -1812,7 +2370,7 @@ function projects() {
1812
2370
  const managed = skills.filter((s) => s.managed);
1813
2371
  const attention = skills.filter((s) => s.state !== "ok" && s.state !== "local only").length;
1814
2372
  return {
1815
- Project: basename6(root),
2373
+ Project: basename8(root),
1816
2374
  Path: dim(tildify(root)),
1817
2375
  Skills: managed.map((s) => s.name).join(", ") || dim("none"),
1818
2376
  "Local only": String(skills.filter((s) => s.state === "local only").length),
@@ -1828,7 +2386,7 @@ function add(args) {
1828
2386
  }
1829
2387
  const root = findProjectRoot();
1830
2388
  if (args.allowTracked) allowTrackedLinks(root);
1831
- printChanges(args.positional.map((name) => addSkill(root, name, { force: args.force })));
2389
+ printChanges(root, args.positional.map((name) => addSkill(root, name, { force: args.force })), args);
1832
2390
  rememberProjects([root]);
1833
2391
  }
1834
2392
  function remove(args) {
@@ -1837,34 +2395,44 @@ function remove(args) {
1837
2395
  process.exit(1);
1838
2396
  }
1839
2397
  const root = findProjectRoot();
1840
- printChanges(args.positional.map((name) => removeSkill(root, name, { force: args.force })));
2398
+ printChanges(root, args.positional.map((name) => removeSkill(root, name, { force: args.force })), args);
1841
2399
  }
1842
2400
  function sync(args) {
1843
2401
  const roots = args.all ? visibleProjects() : [findProjectRoot()];
1844
- for (const root of roots) {
2402
+ const results = roots.map((root) => {
1845
2403
  if (args.allowTracked) allowTrackedLinks(root);
1846
2404
  const changes = syncProject(root, { force: args.force });
1847
- if (args.all) info(`${basename6(root)} ${dim(tildify(root))}`);
1848
- if (changes.length === 0) success("Installed versions match skilllib.json");
1849
- else printChanges(changes);
1850
- }
2405
+ if (!args.json) {
2406
+ 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);
2409
+ }
2410
+ return { project: basename8(root), path: root, changes: withDirs(root, changes) };
2411
+ });
2412
+ if (args.json) json(args.all ? results : results[0].changes);
1851
2413
  if (!args.all) rememberProjects(roots);
1852
2414
  }
1853
2415
  function update(args) {
1854
2416
  const roots = args.all ? visibleProjects() : [findProjectRoot()];
1855
- for (const root of roots) {
2417
+ const results = roots.map((root) => {
1856
2418
  if (args.allowTracked) allowTrackedLinks(root);
1857
2419
  const changes = updateProject(root, args.positional.length ? args.positional : void 0, { force: args.force });
1858
- if (args.all) info(`${basename6(root)} ${dim(tildify(root))}`);
1859
- if (changes.length === 0) success("Everything is on the latest version");
1860
- else printChanges(changes);
1861
- }
2420
+ if (!args.json) {
2421
+ if (args.all) info(`${basename8(root)} ${dim(tildify(root))}`);
2422
+ if (changes.length === 0) success("Everything is on the latest version");
2423
+ else printChanges(root, changes, args);
2424
+ }
2425
+ return { project: basename8(root), path: root, changes: withDirs(root, changes) };
2426
+ });
2427
+ if (args.json) json(args.all ? results : results[0].changes);
1862
2428
  }
1863
2429
  function outdated(args) {
1864
2430
  const roots = args.all ? visibleProjects() : [findProjectRoot()];
1865
- const rows = roots.flatMap(
1866
- (root) => projectStatus(root).filter((s) => s.managed && s.latest !== null && s.version !== null && s.latest > s.version).map((s) => ({ Project: basename6(root), Skill: s.name, Installed: `v${s.version}`, Latest: `v${s.latest}`, Status: colorState(s.state) }))
2431
+ const behind = roots.flatMap(
2432
+ (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 }))
1867
2433
  );
2434
+ if (args.json) return json(behind);
2435
+ const rows = behind.map((b) => ({ Project: b.project, Skill: b.skill, Installed: `v${b.installed}`, Latest: `v${b.latest}`, Status: colorState(b.state) }));
1868
2436
  if (rows.length === 0) return success("Everything is on the latest version");
1869
2437
  table(rows);
1870
2438
  info(dim(`
@@ -1876,12 +2444,15 @@ function importSkills(args) {
1876
2444
  error("Usage: skilllib import <skill-dir>... [--force] | skilllib import --global");
1877
2445
  process.exit(1);
1878
2446
  }
2447
+ const results = [];
1879
2448
  for (const dir of dirs) {
1880
2449
  if (!isSkillDir(dir)) {
1881
2450
  warn(`${tildify(dir)}: no SKILL.md, skipping`);
2451
+ results.push({ path: dir, status: "no SKILL.md" });
1882
2452
  continue;
1883
2453
  }
1884
2454
  const { name, status: result } = importSkill(dir, { force: args.force });
2455
+ results.push({ path: dir, name, status: result });
1885
2456
  if (result === "exists") {
1886
2457
  warn(`${name}: the library already has a different version (use --force to replace it)`);
1887
2458
  continue;
@@ -1889,14 +2460,15 @@ function importSkills(args) {
1889
2460
  success(`${name} ${dim(result === "unchanged" ? "already in library" : result === "added" ? "added to library" : "library updated")}`);
1890
2461
  const skillsDir = resolve4(dir, "..");
1891
2462
  const root = resolve4(skillsDir, "..", "..");
1892
- if (skillsDir === join13(root, ".claude", "skills") && isProjectCandidate(root)) {
2463
+ if (skillsDir === join15(root, ".claude", "skills") && isProjectCandidate(root)) {
1893
2464
  if (readManifest(root).skills[name]?.hash !== treeHash(dir)) {
1894
2465
  const change = addSkill(root, name);
1895
- if (change.action !== "skipped") info(dim(` ${basename6(root)} now depends on ${name} v${change.to}`));
2466
+ if (change.action !== "skipped") info(dim(` ${basename8(root)} now depends on ${name} v${change.to}`));
1896
2467
  }
1897
2468
  rememberProjects([root]);
1898
2469
  }
1899
2470
  }
2471
+ if (args.json) json(results);
1900
2472
  }
1901
2473
  function scan(args) {
1902
2474
  for (const p of args.positional) addRoot(p);
@@ -1907,13 +2479,13 @@ function scan(args) {
1907
2479
  }
1908
2480
  const found = discoverProjects();
1909
2481
  const library = new Set(librarySkills().map((s) => s.name));
1910
- const skillNames = (root) => [...skillDirsIn(projectSkillsDir(root)), ...skillDirsIn(agentsSkillsDir(root))].map((d) => basename6(d));
2482
+ const skillNames = (root) => [...skillDirsIn(projectSkillsDir(root)), ...skillDirsIn(agentsSkillsDir(root))].map((d) => basename8(d));
1911
2483
  const withSkills = found.filter((root) => skillNames(root).length > 0);
1912
2484
  info(`Scanned ${roots.map((r) => tildify(r)).join(", ")}: ${found.length} project(s), ${withSkills.length} with skills
1913
2485
  `);
1914
2486
  table(
1915
2487
  withSkills.map((root) => ({
1916
- Project: basename6(root),
2488
+ Project: basename8(root),
1917
2489
  Path: dim(tildify(root)),
1918
2490
  Skills: [...new Set(skillNames(root))].map((n) => library.has(n) ? n : yellow(n)).join(", ")
1919
2491
  }))
@@ -1954,7 +2526,7 @@ function newSkill(args) {
1954
2526
  error(`Can't create ${name}: ${r.reason}`);
1955
2527
  process.exit(1);
1956
2528
  }
1957
- success(`Created ${tildify(join13(r.dir, "SKILL.md"))}`);
2529
+ success(`Created ${tildify(join15(r.dir, "SKILL.md"))}`);
1958
2530
  }
1959
2531
  function restore(args) {
1960
2532
  const backups = [...listBackups(), ...pluginBackups()];
@@ -1974,9 +2546,34 @@ function restore(args) {
1974
2546
  if (r.ok) success(`Restored ${name} to ${tildify(r.to)}`);
1975
2547
  else error(`Can't restore ${name}: ${r.reason}`);
1976
2548
  }
1977
- function doctor(args) {
2549
+ async function doctor(args) {
1978
2550
  const projects2 = visibleProjects();
1979
- const issues = findIssues(projects2, machineSkills(), new Set(librarySkills().map((s) => s.name)));
2551
+ const machine = machineSkills();
2552
+ const libraryNames = new Set(librarySkills().map((s) => s.name));
2553
+ const issues = findIssues(projects2, machine, libraryNames);
2554
+ if (enabledHarnesses().includes("claude-code")) {
2555
+ const projectOf = projectOfFactory(projects2);
2556
+ const uses = await scanUsage(args.days);
2557
+ const inProject = usesByProject(uses, projectOf);
2558
+ const total = new Map(summarize(uses, projectOf).map((s) => [s.skill, s.uses]));
2559
+ issues.push(
2560
+ ...usageIssues(projects2, machine, libraryNames, {
2561
+ inProject: (root, skill) => inProject.get(root)?.get(skill) ?? 0,
2562
+ total: (skill) => total.get(skill) ?? 0,
2563
+ days: args.days
2564
+ })
2565
+ );
2566
+ }
2567
+ if (args.json) {
2568
+ return json(
2569
+ issues.map(({ fix, choices, ...issue }) => ({
2570
+ ...issue,
2571
+ ...fix && { fix: fix.label },
2572
+ ...choices && { choices: choices.map((c) => c.label) },
2573
+ ...args.fix && fix && { fixed: runFix(fix) }
2574
+ }))
2575
+ );
2576
+ }
1980
2577
  if (issues.length === 0) return success("Everything looks good");
1981
2578
  for (const issue of issues) {
1982
2579
  info(`${issue.severity === "problem" ? red("\u25CF") : yellow("\u25C6")} ${issue.title}`);
@@ -1991,18 +2588,27 @@ function doctor(args) {
1991
2588
  }
1992
2589
  async function usage(args) {
1993
2590
  const summaries = summarize(await scanUsage(args.days), projectOfFactory(knownProjects()));
2591
+ if (args.json) {
2592
+ const library2 = new Set(librarySkills().map((s) => s.name));
2593
+ return json({
2594
+ usageDays: args.days,
2595
+ source: "Claude Code transcripts",
2596
+ skills: summaries.map((s) => ({ skill: s.skill, uses: s.uses, lastUsed: s.lastUsed, projects: [...s.projects].map((p) => basename8(p)), inLibrary: library2.has(s.skill) })),
2597
+ unusedLibrarySkills: librarySkills().filter((l) => !summaries.some((u) => u.skill === l.name)).map((l) => l.name)
2598
+ });
2599
+ }
1994
2600
  if (summaries.length === 0) {
1995
2601
  info(`No skill usage in Claude Code transcripts from the last ${args.days} days.`);
1996
2602
  return;
1997
2603
  }
1998
2604
  const library = new Set(librarySkills().map((s) => s.name));
1999
- info(`Skill usage, last ${args.days} days ${dim(`(from ${tildify(join13(claudeDir(), "projects"))})`)}
2605
+ info(`Skill usage, last ${args.days} days ${dim(`(from ${tildify(join15(claudeDir(), "projects"))})`)}
2000
2606
  `);
2001
2607
  table(
2002
2608
  summaries.map((s) => ({
2003
2609
  Skill: s.skill,
2004
2610
  Uses: String(s.uses),
2005
- Projects: [...s.projects].map((p) => basename6(p)).join(", ") || dim("-"),
2611
+ Projects: [...s.projects].map((p) => basename8(p)).join(", ") || dim("-"),
2006
2612
  "Last used": lastUsed(s),
2007
2613
  Library: library.has(s.skill) ? green("yes") : dim("no")
2008
2614
  }))
@@ -2015,12 +2621,23 @@ async function usage(args) {
2015
2621
  }
2016
2622
  function show(args) {
2017
2623
  const name = args.positional[0];
2018
- const dir = name ? join13(libraryDir(), name) : "";
2624
+ const dir = name ? join15(libraryDir(), name) : "";
2019
2625
  if (!name || !isSkillDir(dir)) {
2020
2626
  error(name ? `${name} is not in the library` : "Usage: skilllib show <skill-name>");
2021
2627
  process.exit(1);
2022
2628
  }
2023
2629
  const { description } = readSkillInfo(dir);
2630
+ if (args.json) {
2631
+ return json({
2632
+ name,
2633
+ description,
2634
+ path: dir,
2635
+ latest: latestVersion(name)?.version ?? null,
2636
+ origin: libraryOrigins()[name] ?? "",
2637
+ projects: knownProjects().filter((p) => name in readManifest(p).skills).map((p) => basename8(p)),
2638
+ skillMd: readFileSync9(join15(dir, "SKILL.md"), "utf-8")
2639
+ });
2640
+ }
2024
2641
  info(`${name} ${dim(tildify(dir))}`);
2025
2642
  if (description) info(`
2026
2643
  ${description}`);
@@ -2051,7 +2668,7 @@ function link(args) {
2051
2668
  for (const root of roots) {
2052
2669
  if (args.allowTracked) allowTrackedLinks(root);
2053
2670
  const res = linkAll(root);
2054
- const label = args.all ? `${basename6(root)}: ` : "";
2671
+ const label = args.all ? `${basename8(root)}: ` : "";
2055
2672
  if (res.linked.length) success(`${label}linked ${res.linked.length} skill(s) for your other agents`);
2056
2673
  else if (!args.all) success("Every skill here is already usable by all your agents");
2057
2674
  if (res.blocked.length) warn(`${label}git tracks ${res.blocked.join(", ")}; re-run with --allow-tracked to link there too`);
@@ -2082,7 +2699,7 @@ ${unreviewed} not reviewed. Keep one global on purpose: skilllib global keep <na
2082
2699
  function tidy(args) {
2083
2700
  const names = args.positional;
2084
2701
  const keepFor = (base) => args.keep ? Object.fromEntries(names.map((n) => [n, base ? resolve4(base, args.keep) : expandHome(args.keep)])) : {};
2085
- const reports = args.global ? [{ label: "global folders", report: planGlobalTidy({ keep: keepFor(null) }) }] : (args.all ? visibleProjects() : [findProjectRoot()]).map((root) => ({ label: basename6(root), report: planProjectTidy(root, { keep: keepFor(root) }) }));
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) }) }));
2086
2703
  let held = 0;
2087
2704
  let changed = 0;
2088
2705
  for (const { label, report } of reports) {
@@ -2112,6 +2729,31 @@ function tidy(args) {
2112
2729
  else if (changed) info(dim("\nReplaced copies are in ~/.skilllib/tidy-backup; `skilllib restore` puts them back."));
2113
2730
  if (held) info(dim(`${held} change${held === 1 ? "" : "s"} held back because git would see them. Re-run with --allow-git to make them.`));
2114
2731
  }
2732
+ function agentSkill(args) {
2733
+ const action = args.positional[0];
2734
+ if (action === "install") {
2735
+ const r = installAgentSkill();
2736
+ if (!r.ok) {
2737
+ error(`Can't install the skilllib skill: ${r.reason}`);
2738
+ process.exit(1);
2739
+ }
2740
+ if (args.json) return json({ state: agentSkillState(), dirs: r.dirs });
2741
+ success(`Installed in ${r.dirs.map((d) => tildify(d)).join(" and ")}`);
2742
+ if (!onPath("skilllib")) warn("`skilllib` isn't on your PATH, so agents can't run it. Install it: npm install -g skilllib");
2743
+ info(dim("New agent sessions can now answer questions like \u201Cwhich skills can you use here?\u201D"));
2744
+ return;
2745
+ }
2746
+ if (action === "remove" || action === "rm") {
2747
+ const removed = removeAgentSkill();
2748
+ if (args.json) return json({ state: agentSkillState(), removed });
2749
+ return removed.length ? success(`Removed from ${removed.map((d) => tildify(d)).join(" and ")}`) : info("The skilllib skill isn't installed.");
2750
+ }
2751
+ const state = agentSkillState();
2752
+ if (args.json) return json({ state, dirs: agentSkillDirs() });
2753
+ if (state === "installed") success(`Your agents can use skilllib ${dim(`(${agentSkillDirs().map((d) => tildify(d)).join(", ")})`)}`);
2754
+ else if (state === "outdated") warn("The skilllib skill is out of date. Run `skilllib agent-skill install` to update it.");
2755
+ else info("Your agents don't know about skilllib yet. Run `skilllib agent-skill install` so you can ask them about your skills.");
2756
+ }
2115
2757
 
2116
2758
  export {
2117
2759
  userHome,
@@ -2138,6 +2780,7 @@ export {
2138
2780
  setKeepGlobal,
2139
2781
  setPluginEnabled,
2140
2782
  machineSkills,
2783
+ skillsLoadedIn,
2141
2784
  libraryOrigins,
2142
2785
  recordOrigin,
2143
2786
  librarySkills,
@@ -2155,17 +2798,29 @@ export {
2155
2798
  deleteLibrarySkill,
2156
2799
  deleteGlobal,
2157
2800
  unloadGlobal,
2801
+ linkGlobal,
2158
2802
  createSkill,
2159
2803
  listBackups,
2160
2804
  restoreBackup,
2805
+ setJsonMode,
2161
2806
  error,
2162
- scanUsage,
2163
2807
  gitInfo,
2164
2808
  relativeTo,
2809
+ claudePlugins,
2165
2810
  claudeBinary,
2811
+ turnOffIn,
2166
2812
  recordRemovedPlugin,
2167
2813
  pluginBackups,
2168
2814
  restorePlugin,
2815
+ cursorPluginSkills,
2816
+ refreshAgentSkill,
2817
+ planProjectTidy,
2818
+ planGlobalTidy,
2819
+ gitVisibleSteps,
2820
+ applyTidy,
2821
+ describeStep,
2822
+ copyLabel,
2823
+ scanUsage,
2169
2824
  parseArgs,
2170
2825
  projectOfFactory,
2171
2826
  status,
@@ -2188,5 +2843,6 @@ export {
2188
2843
  harnessesCommand,
2189
2844
  link,
2190
2845
  globalCommand,
2191
- tidy
2846
+ tidy,
2847
+ agentSkill
2192
2848
  };