skilllib 1.3.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,18 +1,62 @@
1
1
  // src/output.ts
2
- import { sep } from "path";
2
+ import { resolve as resolve2, sep } from "path";
3
+
4
+ // src/paths.ts
5
+ import { realpathSync } from "fs";
3
6
  import { homedir } from "os";
4
- var color = (code) => (s) => process.stdout.isTTY ? `\x1B[${code}m${s}\x1B[0m` : s;
7
+ import { join, resolve } from "path";
8
+ function userHome() {
9
+ return process.env.HOME || homedir();
10
+ }
11
+ function realPath(path) {
12
+ try {
13
+ return realpathSync(resolve(path));
14
+ } catch {
15
+ return resolve(path);
16
+ }
17
+ }
18
+ function skilllibHome() {
19
+ return process.env.SKILLLIB_HOME ?? join(userHome(), ".skilllib");
20
+ }
21
+ function libraryDir() {
22
+ return join(skilllibHome(), "library");
23
+ }
24
+ function claudeDir() {
25
+ return process.env.CLAUDE_CONFIG_DIR ?? join(userHome(), ".claude");
26
+ }
27
+ function codexHome() {
28
+ return process.env.CODEX_HOME?.trim() || join(userHome(), ".codex");
29
+ }
30
+ function codexSystemDir() {
31
+ return process.env.SKILLLIB_CODEX_SYSTEM_DIR ?? "/etc/codex/skills";
32
+ }
33
+ var PROJECT_SKILLS_DIR = ".claude/skills";
34
+ var AGENTS_SKILLS_DIR = ".agents/skills";
35
+ var MANIFEST_FILE = "skilllib.json";
36
+ var OWN_SKILL_MARKER = ".managed-by-skilllib";
37
+
38
+ // src/output.ts
39
+ var jsonMode = false;
40
+ function setJsonMode(on) {
41
+ jsonMode = on;
42
+ }
43
+ function json(data) {
44
+ process.stdout.write(JSON.stringify(data) + "\n");
45
+ }
46
+ var color = (code) => (s) => process.stdout.isTTY && !jsonMode ? `\x1B[${code}m${s}\x1B[0m` : s;
5
47
  var green = color(32);
6
48
  var yellow = color(33);
7
49
  var red = color(31);
8
50
  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}`);
51
+ var out = (msg) => jsonMode ? console.error(msg) : console.log(msg);
52
+ var info = (msg) => out(msg);
53
+ var success = (msg) => out(`${green("\u2713")} ${msg}`);
54
+ var warn = (msg) => out(`${yellow("!")} ${msg}`);
12
55
  var error = (msg) => console.error(`${red("\u2717")} ${msg}`);
13
56
  function tildify(path) {
14
- const home2 = homedir();
15
- return path === home2 || path.startsWith(home2 + sep) ? "~" + path.slice(home2.length) : path;
57
+ for (const home2 of /* @__PURE__ */ new Set([resolve2(userHome()), realPath(userHome())]))
58
+ if (path === home2 || path.startsWith(home2 + sep)) return ("~" + path.slice(home2.length)).replace(/\\/g, "/");
59
+ return path;
16
60
  }
17
61
  function truncate(s, max) {
18
62
  const line = s.replace(/\s+/g, " ");
@@ -28,32 +72,14 @@ function table(rows) {
28
72
  for (const row of rows) info(keys.map((k, i) => pad(row[k] ?? "", widths[i])).join(" ").trimEnd());
29
73
  }
30
74
 
31
- // src/commands.ts
32
- import { basename as basename6, dirname as dirname5, join as join13, resolve as resolve4, sep as sep5 } from "path";
33
-
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";
75
+ // src/agentSkill.ts
76
+ import { existsSync as existsSync8, lstatSync as lstatSync3, mkdirSync as mkdirSync6, readFileSync as readFileSync7, rmSync as rmSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "fs";
77
+ import { join as join9 } from "path";
38
78
 
39
- // src/paths.ts
79
+ // src/config.ts
80
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
40
81
  import { homedir as homedir2 } from "os";
41
- import { join } from "path";
42
- function userHome() {
43
- return process.env.HOME || homedir2();
44
- }
45
- function skilllibHome() {
46
- return process.env.SKILLLIB_HOME ?? join(userHome(), ".skilllib");
47
- }
48
- function libraryDir() {
49
- return join(skilllibHome(), "library");
50
- }
51
- function claudeDir() {
52
- return process.env.CLAUDE_CONFIG_DIR ?? join(userHome(), ".claude");
53
- }
54
- var PROJECT_SKILLS_DIR = ".claude/skills";
55
- var AGENTS_SKILLS_DIR = ".agents/skills";
56
- var MANIFEST_FILE = "skilllib.json";
82
+ import { join as join6, resolve as resolve4 } from "path";
57
83
 
58
84
  // src/harnesses.ts
59
85
  import { existsSync } from "fs";
@@ -76,8 +102,11 @@ var HARNESSES = [
76
102
  short: "Cu",
77
103
  icon: "\u2B21",
78
104
  color: "#E8E8E8",
105
+ // The first folder is where links for Cursor go (see linkEverywhere). Cursor also reads .grok/skills;
106
+ // skilllib leaves that folder to Grok, since it can't tell what Grok needs.
79
107
  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")],
108
+ // Cursor ignores CLAUDE_CONFIG_DIR and CODEX_HOME: it always reads these under your home folder.
109
+ globalDirs: () => [".agents", ".cursor", ".claude", ".codex"].map((d) => join2(home(), d, "skills")),
81
110
  installed: () => existsSync(join2(home(), ".cursor")) || onPath("cursor-agent") || hasApp("Cursor")
82
111
  },
83
112
  {
@@ -86,9 +115,9 @@ var HARNESSES = [
86
115
  short: "Cx",
87
116
  icon: "\u25CE",
88
117
  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")
118
+ projectDirs: [".agents/skills", ".codex/skills"],
119
+ globalDirs: () => [join2(home(), ".agents", "skills"), join2(codexHome(), "skills"), codexSystemDir()],
120
+ installed: () => existsSync(codexHome()) || onPath("codex") || hasApp("Codex")
92
121
  },
93
122
  {
94
123
  id: "zed",
@@ -111,6 +140,7 @@ function hasApp(name) {
111
140
  function harness(id) {
112
141
  return HARNESSES.find((h) => h.id === id);
113
142
  }
143
+ var CURSOR_PICKS = [".cursor", ".claude", ".codex", ".grok", ".agents"];
114
144
  var ALL_PROJECT_DIRS = [...new Set(HARNESSES.flatMap((h) => h.projectDirs))];
115
145
  var PREFERRED_DIRS = [AGENTS_SKILLS_DIR, PROJECT_SKILLS_DIR];
116
146
  function preference(dir) {
@@ -133,15 +163,9 @@ function installDirs(enabled, keep) {
133
163
  return [primary, ...best.filter((d) => d !== primary)];
134
164
  }
135
165
 
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
166
  // src/project.ts
142
167
  import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
143
- import { homedir as homedir3 } from "os";
144
- import { dirname, join as join5, resolve } from "path";
168
+ import { dirname, join as join5, resolve as resolve3 } from "path";
145
169
 
146
170
  // src/versions.ts
147
171
  import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
@@ -149,7 +173,7 @@ import { join as join4 } from "path";
149
173
 
150
174
  // src/skills.ts
151
175
  import { createHash } from "crypto";
152
- import { cpSync, existsSync as existsSync2, readdirSync, readFileSync, realpathSync, rmSync, statSync } from "fs";
176
+ import { cpSync, existsSync as existsSync2, readdirSync, readFileSync, realpathSync as realpathSync2, rmSync, statSync } from "fs";
153
177
  import { basename, join as join3, relative, sep as sep2 } from "path";
154
178
  import { parse as parseYaml } from "yaml";
155
179
  var IGNORED = /* @__PURE__ */ new Set([".DS_Store", ".git", "node_modules"]);
@@ -189,7 +213,7 @@ function treeHash(dir) {
189
213
  }
190
214
  function copySkill(from, to) {
191
215
  rmSync(to, { recursive: true, force: true });
192
- cpSync(realpathSync(from), to, { recursive: true, dereference: true, filter: (src) => !IGNORED.has(basename(src)) });
216
+ cpSync(realpathSync2(from), to, { recursive: true, dereference: true, filter: (src) => !IGNORED.has(basename(src)) });
193
217
  }
194
218
  function skillDirsIn(dir) {
195
219
  if (!existsSync2(dir)) return [];
@@ -248,21 +272,17 @@ function getVersion(name, version) {
248
272
 
249
273
  // src/project.ts
250
274
  function findProjectRoot(from = process.cwd()) {
251
- let dir = resolve(from);
252
- let gitRoot = null;
275
+ let dir = resolve3(from);
253
276
  while (true) {
254
- if (existsSync4(join5(dir, MANIFEST_FILE))) return dir;
255
- if (!gitRoot && existsSync4(join5(dir, ".git"))) gitRoot = dir;
277
+ if (existsSync4(join5(dir, MANIFEST_FILE)) || existsSync4(join5(dir, ".git"))) return dir;
256
278
  const parent = dirname(dir);
257
- if (parent === dir) return gitRoot ?? resolve(from);
279
+ if (parent === dir) return resolve3(from);
258
280
  dir = parent;
259
281
  }
260
282
  }
261
- function projectSkillsDir(root) {
262
- return join5(root, PROJECT_SKILLS_DIR);
263
- }
264
- function agentsSkillsDir(root) {
265
- return join5(root, AGENTS_SKILLS_DIR);
283
+ function projectHere(from = process.cwd()) {
284
+ const root = findProjectRoot(from);
285
+ return isProjectCandidate(root) && (existsSync4(join5(root, ".git")) || existsSync4(join5(root, MANIFEST_FILE))) ? root : null;
266
286
  }
267
287
  function readManifest(root) {
268
288
  const path = join5(root, MANIFEST_FILE);
@@ -302,14 +322,23 @@ function knownProjects() {
302
322
  const path = projectsFile();
303
323
  if (!existsSync4(path)) return [];
304
324
  const list2 = JSON.parse(readFileSync3(path, "utf-8"));
305
- return list2.filter((p) => existsSync4(p) && isProjectCandidate(p));
325
+ return onePerFolder(list2.filter((p) => existsSync4(p) && isProjectCandidate(p)));
326
+ }
327
+ function onePerFolder(paths) {
328
+ const byReal = /* @__PURE__ */ new Map();
329
+ for (const p of paths) {
330
+ const real = realPath(p);
331
+ const seen = byReal.get(real);
332
+ if (seen === void 0 || seen === real && p !== real) byReal.set(real, p);
333
+ }
334
+ return [...new Set(byReal.values())];
306
335
  }
307
336
  function isProjectCandidate(root) {
308
- return resolve(root) !== homedir3();
337
+ return realPath(root) !== realPath(userHome());
309
338
  }
310
339
  function rememberProjects(roots) {
311
- const incoming = roots.map((r) => resolve(r)).filter(isProjectCandidate);
312
- const all = [.../* @__PURE__ */ new Set([...knownProjects(), ...incoming])].sort();
340
+ const incoming = roots.map((r) => resolve3(r)).filter(isProjectCandidate);
341
+ const all = onePerFolder([...knownProjects(), ...incoming]).sort();
313
342
  mkdirSync2(skilllibHome(), { recursive: true });
314
343
  writeFileSync2(projectsFile(), JSON.stringify(all, null, 2) + "\n");
315
344
  }
@@ -331,10 +360,12 @@ function writeConfig(config) {
331
360
  writeFileSync3(configFile(), JSON.stringify(config, null, 2) + "\n");
332
361
  }
333
362
  function expandHome(path) {
334
- return resolve2(path.replace(/^~(?=$|\/)/, homedir4()));
363
+ return resolve4(path.replace(/^~(?=$|\/)/, homedir2()));
335
364
  }
336
365
  function addRoot(path) {
337
366
  const root = expandHome(path);
367
+ if (!existsSync5(root)) throw new Error(`${tildify(root)} doesn't exist`);
368
+ if (!statSync2(root).isDirectory()) throw new Error(`${tildify(root)} isn't a folder`);
338
369
  const config = readConfig();
339
370
  if (!config.roots.includes(root)) writeConfig({ ...config, roots: [...config.roots, root].sort() });
340
371
  return root;
@@ -348,8 +379,10 @@ function removeRoot(path) {
348
379
  }
349
380
  function setHidden(project, hidden) {
350
381
  const config = readConfig();
382
+ if (config.hidden.includes(project) === hidden) return false;
351
383
  const rest = config.hidden.filter((h) => h !== project);
352
384
  writeConfig({ ...config, hidden: hidden ? [...rest, project].sort() : rest });
385
+ return true;
353
386
  }
354
387
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "vendor", "target", "Library", "Applications", "Pictures", "Music", "Movies"]);
355
388
  function findProjects(dir, depth = 4) {
@@ -373,8 +406,8 @@ function discoverProjects() {
373
406
  return visibleProjects();
374
407
  }
375
408
  function visibleProjects() {
376
- const hidden = new Set(readConfig().hidden);
377
- return knownProjects().filter((p) => !hidden.has(p));
409
+ const hidden = new Set(readConfig().hidden.map(realPath));
410
+ return knownProjects().filter((p) => !hidden.has(realPath(p)));
378
411
  }
379
412
  function enabledHarnesses() {
380
413
  return readConfig().harnesses ?? HARNESSES.filter((h) => h.installed()).map((h) => h.id);
@@ -403,9 +436,14 @@ function setKeepGlobal(names, keep) {
403
436
  writeConfig({ ...readConfig(), keepGlobal: [...kept].sort() });
404
437
  }
405
438
 
439
+ // src/library.ts
440
+ import { execFileSync } from "child_process";
441
+ import { existsSync as existsSync7, lstatSync as lstatSync2, mkdirSync as mkdirSync5, readdirSync as readdirSync4, readFileSync as readFileSync6, realpathSync as realpathSync4, renameSync, rmSync as rmSync2, symlinkSync, unlinkSync, writeFileSync as writeFileSync5 } from "fs";
442
+ import { basename as basename3, dirname as dirname2, join as join8, relative as relative2, sep as sep3 } from "path";
443
+
406
444
  // src/sources.ts
407
- import { existsSync as existsSync6, lstatSync, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync5, readlinkSync, realpathSync as realpathSync2, writeFileSync as writeFileSync4 } from "fs";
408
- import { basename as basename2, join as join7, resolve as resolve3 } from "path";
445
+ import { existsSync as existsSync6, lstatSync, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync5, readlinkSync, realpathSync as realpathSync3, writeFileSync as writeFileSync4 } from "fs";
446
+ import { basename as basename2, join as join7, resolve as resolve5 } from "path";
409
447
  function readJson(path) {
410
448
  try {
411
449
  return JSON.parse(readFileSync5(path, "utf-8"));
@@ -414,23 +452,51 @@ function readJson(path) {
414
452
  }
415
453
  }
416
454
  function skillsShLock() {
417
- return readJson(join7(resolve3(claudeDir(), ".."), ".agents", ".skill-lock.json"))?.skills ?? {};
455
+ return readJson(join7(userHome(), ".agents", ".skill-lock.json"))?.skills ?? {};
418
456
  }
419
457
  function projectSkillsLock(root) {
420
458
  return readJson(join7(root, "skills-lock.json"))?.skills ?? {};
421
459
  }
422
460
  function realpathOrNull(path) {
423
461
  try {
424
- return realpathSync2(path);
462
+ return realpathSync3(path);
425
463
  } catch {
426
464
  return null;
427
465
  }
428
466
  }
429
467
  function harnessesReading(dir, enabled) {
430
- return enabled.filter((id) => HARNESSES.find((h) => h.id === id)?.globalDirs().includes(dir));
468
+ const real = realPath(dir);
469
+ return enabled.filter((id) => HARNESSES.find((h) => h.id === id)?.globalDirs().some((d) => realPath(d) === real));
431
470
  }
471
+ var systemDirs = () => [codexSystemDir()];
432
472
  function globalSkillDirs() {
433
- return [...new Set(HARNESSES.flatMap((h) => h.globalDirs()))].filter((d) => d.startsWith(userHome()));
473
+ const seen = /* @__PURE__ */ new Set();
474
+ return [...new Set(HARNESSES.flatMap((h) => h.globalDirs()))].filter((d) => {
475
+ if (systemDirs().includes(d)) return false;
476
+ const real = realPath(d);
477
+ if (seen.has(real)) return false;
478
+ seen.add(real);
479
+ return true;
480
+ });
481
+ }
482
+ function globalDirAs(dir) {
483
+ const real = realPath(dir);
484
+ return globalSkillDirs().find((d) => realPath(d) === real) ?? dir;
485
+ }
486
+ function systemSkills(enabled) {
487
+ return systemDirs().flatMap(
488
+ (dir) => skillDirsIn(dir).map((path) => ({
489
+ name: basename2(path),
490
+ kind: "system",
491
+ origin: dir.replace(/\\/g, "/"),
492
+ path,
493
+ description: readSkillInfo(path).description,
494
+ movable: false,
495
+ broken: false,
496
+ harnesses: harnessesReading(dir, enabled),
497
+ links: []
498
+ }))
499
+ ).filter((s) => s.harnesses.length > 0);
434
500
  }
435
501
  function globalFolderSkills(enabled) {
436
502
  const lock = skillsShLock();
@@ -457,6 +523,10 @@ function globalFolderSkills(enabled) {
457
523
  const target_ = e.isLink ? readlinkSync(e.path).replace(/\\/g, "/") : "";
458
524
  const inAgents = e.dir === agentsDir || target_.includes(".agents/skills");
459
525
  const locked = lock[e.name]?.source;
526
+ if (existsSync6(join7(e.path, OWN_SKILL_MARKER))) {
527
+ skills.push({ name: e.name, kind: "skilllib", origin: "skilllib", path: e.path, description: readSkillInfo(e.path).description, movable: false, broken: false, harnesses: harnessesReading(e.dir, enabled), links: [] });
528
+ continue;
529
+ }
460
530
  skills.push({
461
531
  name: e.name,
462
532
  kind: locked || e.isLink && inAgents ? "skills.sh" : "global",
@@ -478,8 +548,7 @@ function globalFolderSkills(enabled) {
478
548
  return skills;
479
549
  }
480
550
  function tildify2(p) {
481
- const shown = p.startsWith(userHome()) ? "~" + p.slice(userHome().length) : p;
482
- return shown.replace(/\\/g, "/");
551
+ return tildify(p).replace(/\\/g, "/");
483
552
  }
484
553
  function cursorBuiltInSkills(enabled) {
485
554
  if (!enabled.includes("cursor")) return [];
@@ -495,9 +564,10 @@ function cursorBuiltInSkills(enabled) {
495
564
  links: []
496
565
  }));
497
566
  }
498
- function claudeAiSkills() {
567
+ function claudeAiSkills(enabled) {
499
568
  const synced = join7(claudeDir(), "skills", "synced");
500
- if (!existsSync6(synced)) return [];
569
+ const harnesses = harnessesReading(join7(claudeDir(), "skills"), enabled);
570
+ if (!existsSync6(synced) || harnesses.length === 0) return [];
501
571
  return readdirSync3(synced).filter((d) => !d.startsWith(".")).flatMap(
502
572
  (bucket) => skillDirsIn(join7(synced, bucket)).map((path) => ({
503
573
  name: basename2(path),
@@ -507,7 +577,7 @@ function claudeAiSkills() {
507
577
  description: readSkillInfo(path).description,
508
578
  movable: false,
509
579
  broken: false,
510
- harnesses: ["claude-code"],
580
+ harnesses,
511
581
  links: []
512
582
  }))
513
583
  );
@@ -520,11 +590,22 @@ function setPluginEnabled(id, on) {
520
590
  mkdirSync4(claudeDir(), { recursive: true });
521
591
  writeFileSync4(file, JSON.stringify(settings, null, 2) + "\n");
522
592
  }
523
- function pluginSkills() {
593
+ function repoSettingsFiles(root) {
594
+ return [join7(root, ".claude", "settings.json"), join7(root, ".claude", "settings.local.json")];
595
+ }
596
+ function enabledPlugins(root) {
597
+ const files = [join7(claudeDir(), "settings.json"), ...root ? repoSettingsFiles(root) : []];
598
+ return Object.assign({}, ...files.map((f) => readJson(f)?.enabledPlugins ?? {}));
599
+ }
600
+ function pluginSkills(harnesses, root) {
601
+ const claude2 = harnesses.includes("claude-code");
602
+ const cursor = harnesses.includes("cursor");
603
+ if (!claude2 && !cursor) return [];
524
604
  const plugins = join7(claudeDir(), "plugins");
525
- const enabled = readJson(join7(claudeDir(), "settings.json"))?.enabledPlugins ?? {};
526
605
  const installed = readJson(join7(plugins, "installed_plugins.json"))?.plugins ?? {};
527
- const fromMarketplaces = Object.entries(enabled).filter(([, on]) => on).flatMap(([id]) => {
606
+ const user = enabledPlugins();
607
+ const enabled = root ? enabledPlugins(root) : user;
608
+ const fromMarketplaces = [.../* @__PURE__ */ new Set([...Object.keys(enabled), ...Object.keys(user)])].filter((id) => claude2 && enabled[id] || cursor && user[id]).flatMap((id) => {
528
609
  const [name = "", marketplace = ""] = id.split("@");
529
610
  const candidates = [
530
611
  ...(installed[id] ?? []).flatMap((i) => i.installPath ? [i.installPath] : []),
@@ -532,19 +613,22 @@ function pluginSkills() {
532
613
  join7(plugins, "marketplaces", marketplace, "external_plugins", name),
533
614
  join7(plugins, "marketplaces", marketplace)
534
615
  ];
535
- const root = candidates.find((c) => existsSync6(join7(c, "skills")));
536
- return root ? skillDirsIn(join7(root, "skills")).map((path) => ({ path, origin: id })) : [];
616
+ const dir = candidates.find((c) => existsSync6(join7(c, "skills")));
617
+ if (!dir) return [];
618
+ const inCursor = cursor && user[id] && (installed[id] ?? []).some((i) => i.scope === "user" && i.installPath === dir);
619
+ const loadedBy = [...claude2 && enabled[id] ? ["claude-code"] : [], ...inCursor ? ["cursor"] : []];
620
+ return loadedBy.length ? skillDirsIn(join7(dir, "skills")).map((path) => ({ path, origin: id, loadedBy })) : [];
537
621
  });
538
622
  const syncedDir = join7(plugins, "synced");
539
- const fromSynced = existsSync6(syncedDir) ? readdirSync3(syncedDir).filter((b) => !b.startsWith(".")).flatMap(
623
+ const fromSynced = claude2 && existsSync6(syncedDir) ? readdirSync3(syncedDir).filter((b) => !b.startsWith(".")).flatMap(
540
624
  (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;
625
+ const root2 = join7(syncedDir, bucket, d.name);
626
+ const name = readJson(join7(root2, ".claude-plugin", "plugin.json"))?.name ?? d.name;
543
627
  if (enabled[`${name}@synced`] === false) return [];
544
- return skillDirsIn(join7(root, "skills")).map((path) => ({ path, origin: `${name}@synced` }));
628
+ return skillDirsIn(join7(root2, "skills")).map((path) => ({ path, origin: `${name}@synced`, loadedBy: ["claude-code"] }));
545
629
  })
546
630
  ) : [];
547
- return [...fromMarketplaces, ...fromSynced].map(({ path, origin }) => ({
631
+ return [...fromMarketplaces, ...fromSynced].map(({ path, origin, loadedBy }) => ({
548
632
  name: basename2(path),
549
633
  kind: "plugin",
550
634
  origin,
@@ -552,20 +636,24 @@ function pluginSkills() {
552
636
  description: readSkillInfo(path).description,
553
637
  movable: false,
554
638
  broken: false,
555
- harnesses: ["claude-code"],
639
+ harnesses: loadedBy,
556
640
  links: []
557
641
  }));
558
642
  }
643
+ var byKindAndName = (a, b) => a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name);
559
644
  function machineSkills(enabled = enabledHarnesses()) {
560
- const claude2 = enabled.includes("claude-code");
561
645
  return [
562
646
  ...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
- );
647
+ ...claudeAiSkills(enabled),
648
+ ...pluginSkills(enabled),
649
+ ...cursorBuiltInSkills(enabled),
650
+ ...systemSkills(enabled)
651
+ ].sort(byKindAndName);
652
+ }
653
+ function skillsLoadedIn(root, machine, enabled = enabledHarnesses()) {
654
+ if (!enabled.includes("claude-code")) return machine;
655
+ if (!repoSettingsFiles(root).some((f) => readJson(f)?.enabledPlugins)) return machine;
656
+ return [...machine.filter((m) => m.kind !== "plugin"), ...pluginSkills(enabled, root)].sort(byKindAndName);
569
657
  }
570
658
  function originsFile() {
571
659
  return join7(skilllibHome(), "origins.json");
@@ -580,7 +668,7 @@ function recordOrigin(name, origin) {
580
668
  writeFileSync4(originsFile(), JSON.stringify({ ...origins, [name]: origin }, null, 2) + "\n");
581
669
  }
582
670
  function originFor(dir) {
583
- const match = machineSkills().find((s) => resolve3(s.path) === resolve3(dir));
671
+ const match = machineSkills().find((s) => resolve5(s.path) === resolve5(dir));
584
672
  if (match) return match.kind === "global" ? "global folder" : `${match.kind}: ${match.origin}`;
585
673
  const project = dir.split(/[\\/]\.(?:claude|agents)[\\/]skills[\\/]/)[0];
586
674
  if (!project || project === dir) return dir;
@@ -601,7 +689,7 @@ function librarySkillDir(name) {
601
689
  }
602
690
  function realpathOrNull2(path) {
603
691
  try {
604
- return realpathSync3(path);
692
+ return realpathSync4(path);
605
693
  } catch {
606
694
  return null;
607
695
  }
@@ -634,8 +722,8 @@ function dependencyDirs(root, name, dep) {
634
722
  }
635
723
  function isGitTracked(root, dir) {
636
724
  try {
637
- const out = execFileSync("git", ["ls-files", "--", dir], { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
638
- return out.trim().length > 0;
725
+ const out2 = execFileSync("git", ["ls-files", "--", dir], { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
726
+ return out2.trim().length > 0;
639
727
  } catch {
640
728
  return false;
641
729
  }
@@ -676,18 +764,13 @@ function projectStatus(root) {
676
764
  for (const [name, paths] of byName) {
677
765
  const real = paths.find((p) => !isLink(p)) ?? paths[0];
678
766
  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
767
  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
768
  unmanaged.push({
686
769
  name,
687
770
  managed: false,
688
- state,
771
+ state: unmanagedState(name, real, location === AGENTS_SKILLS_DIR, source),
689
772
  version: null,
690
- latest: latest?.version ?? null,
773
+ latest: latestVersion(name)?.version ?? null,
691
774
  location,
692
775
  path: real,
693
776
  visibility: visibilityOf(root, name, enabled),
@@ -696,20 +779,89 @@ function projectStatus(root) {
696
779
  }
697
780
  return [...managed, ...unmanaged].sort((a, b) => a.name.localeCompare(b.name));
698
781
  }
699
- function linkEverywhere(root, name, location, { allowTracked = false } = {}) {
782
+ function unmanagedState(name, real, committed, source) {
783
+ if (source) return "from npx skills";
784
+ const latest = latestVersion(name);
785
+ const local = treeHash(real);
786
+ const sameAsLibrary = latest !== null && (latest.hash === local || versionForHash(name, local ?? "") !== null);
787
+ if (committed) return latest === null ? "repo skill" : sameAsLibrary ? "repo skill, in library" : "repo skill, differs from library";
788
+ return latest === null ? "local only" : sameAsLibrary ? "untracked copy of library skill" : "untracked, differs from library";
789
+ }
790
+ var NESTED_READERS = [
791
+ [PROJECT_SKILLS_DIR, ["claude-code", "cursor"]],
792
+ [AGENTS_SKILLS_DIR, ["codex", "cursor"]],
793
+ [".cursor/skills", ["cursor"]],
794
+ [".codex/skills", ["codex", "cursor"]]
795
+ ];
796
+ var NESTED_SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "vendor", "target", "coverage"]);
797
+ function nestedSkills(root, { depth = 3, enabled = enabledHarnesses() } = {}) {
798
+ const found = [];
799
+ const walk = (dir, left) => {
800
+ let subs = [];
801
+ try {
802
+ subs = readdirSync4(dir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".") && !NESTED_SKIP.has(d.name)).map((d) => join8(dir, d.name));
803
+ } catch {
804
+ return;
805
+ }
806
+ for (const sub of subs) {
807
+ if (existsSync7(join8(sub, ".git"))) continue;
808
+ for (const [skillsDir, readers] of NESTED_READERS) {
809
+ const location = relative2(root, join8(sub, skillsDir)).split(sep3).join("/");
810
+ for (const path of skillDirsIn(join8(sub, skillsDir))) {
811
+ if (isLink(path) && (realpathOrNull2(path) ?? "").startsWith((realpathOrNull2(root) ?? root) + sep3)) continue;
812
+ const name = basename3(path);
813
+ found.push({
814
+ name,
815
+ managed: false,
816
+ state: unmanagedState(name, path, skillsDir === AGENTS_SKILLS_DIR, void 0),
817
+ version: null,
818
+ latest: latestVersion(name)?.version ?? null,
819
+ location,
820
+ path,
821
+ visibility: enabled.map((id) => ({ id, paths: readers.includes(id) ? 1 : 0 }))
822
+ });
823
+ }
824
+ }
825
+ if (left > 1) walk(sub, left - 1);
826
+ }
827
+ };
828
+ walk(root, depth);
829
+ return found.sort((a, b) => a.location.localeCompare(b.location) || a.name.localeCompare(b.name));
830
+ }
831
+ function sharedByGit(root, location, name) {
832
+ const copy = `${location}/${name}`;
833
+ if (isGitTracked(root, copy)) return true;
834
+ if (!isGitTracked(root, location)) return false;
835
+ try {
836
+ execFileSync("git", ["check-ignore", "-q", "--no-index", "--", copy], { cwd: root, stdio: "ignore" });
837
+ return false;
838
+ } catch {
839
+ return true;
840
+ }
841
+ }
842
+ function linkEverywhere(root, name, location, { allowTracked = false, dryRun = false } = {}) {
700
843
  const enabled = enabledHarnesses();
701
- const result = { created: [], blocked: [] };
844
+ const result = { created: [], blocked: [], uncommitted: [] };
845
+ let shared;
702
846
  for (const id of enabled) {
703
847
  if (visibilityOf(root, name, [id])[0].paths > 0) continue;
704
848
  const dir = harness(id).projectDirs[0];
705
849
  const link2 = join8(root, dir, name);
706
- if (entryExists(link2)) continue;
707
- if (!allowTracked && isGitTracked(root, dir) && !readConfig().agentsDirOk?.includes(root)) {
708
- result.blocked.push(dir);
709
- continue;
850
+ if (entryExists(link2) || Object.values(result).some((dirs) => dirs.includes(dir))) continue;
851
+ if (isGitTracked(root, dir)) {
852
+ if (!(shared ??= sharedByGit(root, location, name))) {
853
+ if (!result.uncommitted.includes(dir)) result.uncommitted.push(dir);
854
+ continue;
855
+ }
856
+ if (!allowTracked && !readConfig().agentsDirOk?.includes(root)) {
857
+ if (!result.blocked.includes(dir)) result.blocked.push(dir);
858
+ continue;
859
+ }
860
+ }
861
+ if (!dryRun) {
862
+ mkdirSync5(join8(root, dir), { recursive: true });
863
+ linkDir(join8(root, location, name), link2);
710
864
  }
711
- mkdirSync5(join8(root, dir), { recursive: true });
712
- linkDir(join8(root, location, name), link2);
713
865
  result.created.push(dir);
714
866
  }
715
867
  return result;
@@ -725,7 +877,7 @@ function unlinkEverywhere(root, name) {
725
877
  function relinkDependency(root, name, { allowTracked = false } = {}) {
726
878
  const manifest = readManifest(root);
727
879
  const dep = manifest.skills[name];
728
- if (!dep) return { created: [], blocked: [] };
880
+ if (!dep) return { created: [], blocked: [], uncommitted: [] };
729
881
  const result = linkEverywhere(root, name, dep.dir ?? PROJECT_SKILLS_DIR, { allowTracked });
730
882
  if (result.created.length) {
731
883
  manifest.skills[name] = { ...dep, links: [.../* @__PURE__ */ new Set([...dep.links ?? [], ...result.created])] };
@@ -736,13 +888,15 @@ function relinkDependency(root, name, { allowTracked = false } = {}) {
736
888
  function linkAll(root, { allowTracked = false } = {}) {
737
889
  const linked = [];
738
890
  const blocked = /* @__PURE__ */ new Set();
891
+ const uncommitted = [];
739
892
  for (const skill of projectStatus(root)) {
740
893
  if (!skill.visibility.some((v) => v.paths === 0) || skill.state === "folder missing") continue;
741
894
  const res = skill.managed ? relinkDependency(root, skill.name, { allowTracked }) : linkEverywhere(root, skill.name, skill.location, { allowTracked });
742
895
  if (res.created.length) linked.push({ name: skill.name, into: res.created });
743
896
  for (const dir of res.blocked) blocked.add(dir);
897
+ if (res.uncommitted.length) uncommitted.push(skill.name);
744
898
  }
745
- return { linked, blocked: [...blocked] };
899
+ return { linked, blocked: [...blocked], uncommitted };
746
900
  }
747
901
  function addSkill(root, name, { force = false, version, allowTracked = false } = {}) {
748
902
  const target = version === void 0 ? latestVersion(name) : getVersion(name, version);
@@ -760,13 +914,17 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
760
914
  reason: recorded ? "has local edits (use --force to overwrite, or save them to the library first)" : `${primary}/${name} already exists with different content (use --force to replace it)`
761
915
  };
762
916
  }
917
+ let backedUp;
763
918
  if (local !== target.hash || isLink(to)) {
764
919
  mkdirSync5(join8(root, primary), { recursive: true });
765
920
  if (isLink(to)) unlinkSync(to);
921
+ else if (local !== null && !knownContent) backedUp = stash(to, "edit-backup");
766
922
  copySkill(versionDir(name, target.version), to);
767
923
  }
768
924
  const links = [];
769
925
  const blocked = [];
926
+ const uncommitted = [];
927
+ let shared;
770
928
  for (const dir of linkDirs) {
771
929
  const link2 = join8(root, dir, name);
772
930
  if (isLink(link2) && realpathOrNull2(link2) === realpathOrNull2(to)) {
@@ -774,9 +932,15 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
774
932
  continue;
775
933
  }
776
934
  if (entryExists(link2)) continue;
777
- if (!allowTracked && !recorded?.links?.includes(dir) && isGitTracked(root, dir) && !readConfig().agentsDirOk?.includes(root)) {
778
- blocked.push(dir);
779
- continue;
935
+ if (!recorded?.links?.includes(dir) && isGitTracked(root, dir)) {
936
+ if (!(shared ??= sharedByGit(root, primary, name))) {
937
+ uncommitted.push(dir);
938
+ continue;
939
+ }
940
+ if (!allowTracked && !readConfig().agentsDirOk?.includes(root)) {
941
+ blocked.push(dir);
942
+ continue;
943
+ }
780
944
  }
781
945
  mkdirSync5(join8(root, dir), { recursive: true });
782
946
  linkDir(to, link2);
@@ -791,10 +955,12 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
791
955
  writeManifest(root, manifest);
792
956
  return {
793
957
  name,
794
- action: recorded || local !== null ? "updated" : "installed",
958
+ action: !recorded && local === null ? "installed" : backedUp && recorded?.version === target.version ? "reset" : "updated",
795
959
  ...recorded ? { from: recorded.version } : {},
796
960
  to: target.version,
797
- ...blocked.length ? { blocked } : {}
961
+ ...blocked.length ? { blocked } : {},
962
+ ...uncommitted.length ? { uncommitted } : {},
963
+ ...backedUp ? { backedUp } : {}
798
964
  };
799
965
  }
800
966
  function removeSkill(root, name, { force = false } = {}) {
@@ -803,17 +969,17 @@ function removeSkill(root, name, { force = false } = {}) {
803
969
  if (!recorded) return { name, action: "skipped", reason: "not managed by skilllib in this project" };
804
970
  const dir = join8(root, recorded.dir ?? PROJECT_SKILLS_DIR, name);
805
971
  const local = treeHash(dir);
806
- if (local !== null && local !== recorded.hash && !versionForHash(name, local) && !force) {
807
- return { name, action: "skipped", reason: "has local edits (use --force to delete anyway)" };
808
- }
972
+ const edited = local !== null && local !== recorded.hash && !versionForHash(name, local);
973
+ if (edited && !force) return { name, action: "skipped", reason: "has local edits (use --force to delete anyway)" };
809
974
  for (const linkDir2 of recorded.links ?? []) {
810
975
  const link2 = join8(root, linkDir2, name);
811
976
  if (isLink(link2)) unlinkSync(link2);
812
977
  }
813
- rmSync2(dir, { recursive: true, force: true });
978
+ const backedUp = edited ? stash(dir, "edit-backup") : void 0;
979
+ if (!backedUp) rmSync2(dir, { recursive: true, force: true });
814
980
  delete manifest.skills[name];
815
981
  writeManifest(root, manifest);
816
- return { name, action: "removed" };
982
+ return { name, action: "removed", ...backedUp ? { backedUp } : {} };
817
983
  }
818
984
  function syncProject(root, { force = false } = {}) {
819
985
  const { skills } = readManifest(root);
@@ -824,9 +990,18 @@ function syncProject(root, { force = false } = {}) {
824
990
  const pinned = dep.version > 0 && getVersion(name, dep.version) ? dep.version : void 0;
825
991
  if (local !== null && local !== dep.hash && !force) return [{ name, action: "skipped", reason: "has local edits (use --force to reset them)" }];
826
992
  const change = addSkill(root, name, { force: true, version: pinned });
827
- return change.action === "skipped" ? [change] : [{ ...change, action: local === null ? "installed" : "updated" }];
993
+ if (change.action === "skipped") return [{ ...change, reason: `not in your library (${tildify(libraryDir())}); import it, or set SKILLLIB_HOME to the library that has it` }];
994
+ return [{ ...change, action: local === null ? "installed" : change.action }];
828
995
  });
829
996
  }
997
+ function discardEdits(root, name) {
998
+ const dep = readManifest(root).skills[name];
999
+ if (!dep) return { name, action: "skipped", reason: "not managed by skilllib in this project" };
1000
+ const dir = join8(root, dep.dir ?? PROJECT_SKILLS_DIR, name);
1001
+ const pinned = dep.version > 0 && getVersion(name, dep.version) ? dep.version : void 0;
1002
+ if (existsSync7(dir) && !isLink(dir)) stash(dir, "edit-backup");
1003
+ return addSkill(root, name, { force: true, version: pinned });
1004
+ }
830
1005
  function updateProject(root, names, { force = false } = {}) {
831
1006
  const { skills } = readManifest(root);
832
1007
  return Object.keys(skills).filter((name) => !names || names.includes(name)).flatMap((name) => {
@@ -835,6 +1010,7 @@ function updateProject(root, names, { force = false } = {}) {
835
1010
  const dep = skills[name];
836
1011
  const local = treeHash(join8(root, dep.dir ?? PROJECT_SKILLS_DIR, name));
837
1012
  if (dep.hash === latest.hash && local === latest.hash) return [];
1013
+ if (dep.hash === latest.hash && local !== null && !force) return [];
838
1014
  const change = addSkill(root, name, { force });
839
1015
  return change.action === "skipped" || change.from !== change.to || local !== latest.hash ? [change] : [];
840
1016
  });
@@ -847,9 +1023,9 @@ function importSkill(dir, { force = false } = {}) {
847
1023
  if (existing === incoming) return { name, status: "unchanged" };
848
1024
  if (existing !== null && !force) return { name, status: "exists" };
849
1025
  mkdirSync5(libraryDir(), { recursive: true });
850
- const origin = originFor(dir);
1026
+ const origin = existing !== null && libraryOrigins()[name] ? null : originFor(dir);
851
1027
  copySkill(dir, to);
852
- recordOrigin(name, origin);
1028
+ if (origin) recordOrigin(name, origin);
853
1029
  forgetLatest(name);
854
1030
  latestVersion(name);
855
1031
  return { name, status: existing === null ? "added" : "updated" };
@@ -903,6 +1079,23 @@ function unloadGlobal(path, links = [], { requireLibrary = true } = {}) {
903
1079
  if (!globalSkillDirs().some((d) => entryExists(join8(d, name)))) setKeepGlobal([name], false);
904
1080
  return { ok: true, movedTo };
905
1081
  }
1082
+ function linkGlobal(path, agents) {
1083
+ const name = basename3(path);
1084
+ const linked = [];
1085
+ const skipped = [];
1086
+ for (const dir of new Set(agents.map((id) => harness(id).globalDirs()[0]))) {
1087
+ const link2 = join8(dir, name);
1088
+ if (isLink(link2) && realpathOrNull2(link2) === null) unlinkSync(link2);
1089
+ else if (entryExists(link2)) {
1090
+ if (realpathOrNull2(link2) !== realpathOrNull2(path)) skipped.push(link2);
1091
+ continue;
1092
+ }
1093
+ mkdirSync5(dir, { recursive: true });
1094
+ linkDir(path, link2);
1095
+ linked.push(link2);
1096
+ }
1097
+ return { linked, skipped };
1098
+ }
906
1099
  function createSkill(name, description) {
907
1100
  if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) return { ok: false, reason: "use lowercase letters, digits and dashes" };
908
1101
  const dir = librarySkillDir(name);
@@ -912,19 +1105,19 @@ function createSkill(name, description) {
912
1105
  join8(dir, "SKILL.md"),
913
1106
  `---
914
1107
  name: ${name}
915
- description: ${description || "Describe when Claude should use this skill."}
1108
+ description: ${description || "Describe when an agent should use this skill."}
916
1109
  ---
917
1110
 
918
1111
  # ${name}
919
1112
 
920
- Instructions for Claude go here.
1113
+ Instructions for the agent go here.
921
1114
  `
922
1115
  );
923
1116
  forgetLatest(name);
924
1117
  return { ok: true, dir };
925
1118
  }
926
1119
  function listBackups() {
927
- return ["trash", "global-backup", "tidy-backup"].flatMap((kind) => {
1120
+ return ["trash", "global-backup", "tidy-backup", "edit-backup"].flatMap((kind) => {
928
1121
  const dir = join8(skilllibHome(), kind);
929
1122
  if (!existsSync7(dir)) return [];
930
1123
  const index = readBackupIndex(kind);
@@ -935,11 +1128,16 @@ function listBackups() {
935
1128
  const from = index[entry] ?? (kind === "trash" ? librarySkillDir(name) : join8(claudeDir(), "skills", name));
936
1129
  return [{ name, kind, path: join8(dir, entry), movedAt: `${match[2]} ${match[3]}:${match[4]}`, from }];
937
1130
  });
938
- }).sort((a, b) => b.movedAt.localeCompare(a.movedAt));
1131
+ }).sort((a, b) => b.path.slice(-24).localeCompare(a.path.slice(-24)));
1132
+ }
1133
+ function backupFrom(backup) {
1134
+ if (backup.kind === "plugin") return "Claude Code plugin";
1135
+ return backup.from === librarySkillDir(backup.name) ? "library (deleted)" : tildify(dirname2(backup.from));
939
1136
  }
940
1137
  function restoreBackup(backup) {
941
1138
  const to = backup.from;
942
1139
  if (backup.kind === "tidy-backup" && isLink(to)) unlinkSync(to);
1140
+ if (backup.kind === "edit-backup" && !isLink(to) && versionForHash(backup.name, treeHash(to) ?? "") !== null) rmSync2(to, { recursive: true, force: true });
943
1141
  let taken = existsSync7(to);
944
1142
  try {
945
1143
  taken = taken || lstatSync2(to) !== null;
@@ -952,125 +1150,72 @@ function restoreBackup(backup) {
952
1150
  return { ok: true, to };
953
1151
  }
954
1152
 
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;
1153
+ // src/agentSkill.ts
1154
+ var AGENT_SKILL_NAME = "skilllib";
1155
+ function shippedSkill() {
1156
+ return readFileSync7(new URL("../skills/skilllib/SKILL.md", import.meta.url), "utf-8");
1003
1157
  }
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 [];
1158
+ function agentSkillDirs(enabled = enabledHarnesses()) {
1159
+ const claudeSkills = join9(claudeDir(), "skills");
1160
+ const agentsSkills = join9(userHome(), ".agents", "skills");
1161
+ const needsClaude = enabled.some((id) => harness(id).globalDirs().includes(claudeSkills));
1162
+ const needsAgents = enabled.some((id) => !harness(id).globalDirs().includes(claudeSkills));
1163
+ return [...needsClaude ? [claudeSkills] : [], ...needsAgents ? [agentsSkills] : []].map((d) => join9(d, AGENT_SKILL_NAME));
1035
1164
  }
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
- });
1165
+ function isOurs(dir) {
1166
+ return existsSync8(join9(dir, OWN_SKILL_MARKER));
1044
1167
  }
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
- }
1168
+ function isDangling(path) {
1169
+ return entryExists2(path) && lstatSync3(path).isSymbolicLink() && !existsSync8(path);
1170
+ }
1171
+ function entryExists2(path) {
1172
+ try {
1173
+ lstatSync3(path);
1174
+ return true;
1175
+ } catch {
1176
+ return false;
1055
1177
  }
1056
- return [...seen.values()];
1057
1178
  }
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);
1179
+ function agentSkillState(enabled = enabledHarnesses()) {
1180
+ const [primary, ...links] = agentSkillDirs(enabled);
1181
+ if (!primary || !isOurs(primary) || links.some((l) => !existsSync8(join9(l, "SKILL.md")))) return "missing";
1182
+ return readFileSync7(join9(primary, "SKILL.md"), "utf-8") === shippedSkill() ? "installed" : "outdated";
1183
+ }
1184
+ function installAgentSkill(enabled = enabledHarnesses()) {
1185
+ const [primary, ...links] = agentSkillDirs(enabled);
1186
+ if (!primary) return { ok: false, reason: "no agents chosen yet (skilllib harnesses <id>...)" };
1187
+ const foreign = [primary, ...links].find((d) => entryExists2(d) && !isOurs(d) && !isDangling(d));
1188
+ if (foreign) return { ok: false, reason: `${tildify(foreign)} already exists and isn't skilllib's` };
1189
+ for (const d of [primary, ...links]) if (isDangling(d)) unlinkSync2(d);
1190
+ mkdirSync6(primary, { recursive: true });
1191
+ writeFileSync6(join9(primary, "SKILL.md"), shippedSkill());
1192
+ writeFileSync6(join9(primary, OWN_SKILL_MARKER), "Installed by skilllib. `skilllib agent-skill remove` removes it.\n");
1193
+ for (const link2 of links) {
1194
+ if (entryExists2(link2)) continue;
1195
+ mkdirSync6(join9(link2, ".."), { recursive: true });
1196
+ linkDir(primary, link2);
1197
+ }
1198
+ return { ok: true, dirs: [primary, ...links] };
1199
+ }
1200
+ function removeAgentSkill() {
1201
+ const all = agentSkillDirs(["claude-code", "codex"]);
1202
+ const removed = all.filter((d) => entryExists2(d) && (isOurs(d) || isDangling(d)));
1203
+ for (const d of removed) {
1204
+ if (lstatSync3(d).isSymbolicLink()) unlinkSync2(d);
1205
+ else rmSync3(d, { recursive: true, force: true });
1206
+ }
1207
+ return removed;
1208
+ }
1209
+ function refreshAgentSkill() {
1210
+ try {
1211
+ if (agentSkillState() === "outdated") installAgentSkill();
1212
+ } catch {
1068
1213
  }
1069
- return [...bySkill.values()].sort((a, b) => b.uses - a.uses);
1070
1214
  }
1071
1215
 
1072
- // src/health.ts
1073
- import { basename as basename5 } from "path";
1216
+ // src/commands.ts
1217
+ import { existsSync as existsSync13, readFileSync as readFileSync9 } from "fs";
1218
+ import { basename as basename8, join as join14, resolve as resolve7, sep as sep5 } from "path";
1074
1219
 
1075
1220
  // src/git.ts
1076
1221
  import { execFileSync as execFileSync2 } from "child_process";
@@ -1080,21 +1225,21 @@ function git(root, args, input) {
1080
1225
  try {
1081
1226
  return execFileSync2("git", args, { cwd: root, encoding: "utf-8", input, stdio: ["pipe", "pipe", "ignore"], maxBuffer: 16 * 1024 * 1024 });
1082
1227
  } catch (err) {
1083
- const out = err.stdout;
1084
- return typeof out === "string" ? out : null;
1228
+ const out2 = err.stdout;
1229
+ return typeof out2 === "string" ? out2 : null;
1085
1230
  }
1086
1231
  }
1087
- function gitInfo(root) {
1232
+ function gitInfo(root, extraDirs = []) {
1088
1233
  if (!existsSync9(join10(root, ".git"))) return null;
1089
- const dirs = [...ALL_PROJECT_DIRS, MANIFEST_FILE].filter((d) => existsSync9(join10(root, d)));
1234
+ const dirs = [...ALL_PROJECT_DIRS, MANIFEST_FILE, ...extraDirs].filter((d) => existsSync9(join10(root, d)));
1090
1235
  if (dirs.length === 0) return { of: () => "new" };
1091
1236
  const tracked = (git(root, ["ls-files", "-z", "--", ...dirs]) ?? "").split("\0").filter(Boolean);
1092
1237
  const dirty = (git(root, ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...dirs]) ?? "").split("\0").filter(Boolean).map((entry) => entry.slice(3));
1093
1238
  const ignoredCache = /* @__PURE__ */ new Map();
1094
1239
  const isIgnored = (rel) => {
1095
1240
  if (!ignoredCache.has(rel)) {
1096
- const out = git(root, ["check-ignore", "--no-index", "--", rel]) ?? "";
1097
- ignoredCache.set(rel, out.trim().length > 0);
1241
+ const out2 = git(root, ["check-ignore", "--no-index", "--", rel]) ?? "";
1242
+ ignoredCache.set(rel, out2.trim().length > 0);
1098
1243
  }
1099
1244
  return ignoredCache.get(rel);
1100
1245
  };
@@ -1111,20 +1256,28 @@ function relativeTo(root, path) {
1111
1256
  return relative3(root, path).split("\\").join("/");
1112
1257
  }
1113
1258
 
1259
+ // src/here.ts
1260
+ import { readdirSync as readdirSync7 } from "fs";
1261
+ import { basename as basename7 } from "path";
1262
+
1263
+ // src/health.ts
1264
+ import { lstatSync as lstatSync4 } from "fs";
1265
+ import { basename as basename6, dirname as dirname5 } from "path";
1266
+
1114
1267
  // src/plugins.ts
1115
1268
  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";
1117
- import { basename as basename4, dirname as dirname3, join as join11 } from "path";
1269
+ import { existsSync as existsSync10, mkdirSync as mkdirSync7, readdirSync as readdirSync5, readFileSync as readFileSync8, statSync as statSync3, writeFileSync as writeFileSync7 } from "fs";
1270
+ import { basename as basename4, dirname as dirname3, join as join11, resolve as resolve6 } from "path";
1118
1271
  function readJson2(path) {
1119
1272
  try {
1120
- return JSON.parse(readFileSync7(path, "utf-8"));
1273
+ return JSON.parse(readFileSync8(path, "utf-8"));
1121
1274
  } catch {
1122
1275
  return null;
1123
1276
  }
1124
1277
  }
1125
1278
  function nonEmptyDir(path) {
1126
1279
  try {
1127
- return readdirSync6(path).some((f) => !f.startsWith("."));
1280
+ return readdirSync5(path).some((f) => !f.startsWith("."));
1128
1281
  } catch {
1129
1282
  return false;
1130
1283
  }
@@ -1138,14 +1291,15 @@ function extrasOf(root) {
1138
1291
  ...nonEmptyDir(join11(root, "commands")) || manifest.commands ? ["commands"] : []
1139
1292
  ];
1140
1293
  }
1141
- function claudePlugins(machine) {
1294
+ function claudePlugins(machine, root) {
1142
1295
  const installed = readJson2(join11(claudeDir(), "plugins", "installed_plugins.json"))?.plugins ?? {};
1143
1296
  const byOrigin = /* @__PURE__ */ new Map();
1144
1297
  for (const m of machine) if (m.kind === "plugin") byOrigin.set(m.origin, [...byOrigin.get(m.origin) ?? [], m.path]);
1145
1298
  return [...byOrigin].map(([origin, skills]) => {
1146
1299
  const synced = origin.endsWith("@synced");
1147
1300
  const id = origin;
1148
- const entry = installed[origin]?.[0];
1301
+ const entries = installed[origin] ?? [];
1302
+ const entry = entries.find((e) => root && e.projectPath === root) ?? entries.find((e) => (e.scope ?? "user") === "user") ?? entries[0];
1149
1303
  return {
1150
1304
  id,
1151
1305
  scope: synced ? null : entry?.scope ?? "user",
@@ -1166,8 +1320,7 @@ function claudeBinary() {
1166
1320
  }
1167
1321
  }) ?? null;
1168
1322
  }
1169
- function claude(args, cwd) {
1170
- const bin = claudeBinary();
1323
+ function claude(args, cwd, bin = claudeBinary()) {
1171
1324
  if (!bin) return { ok: false, reason: `Claude Code's CLI didn't run (tried \`claude\` on your PATH and ~/.local/bin/claude); run \`claude ${args.join(" ")}\` yourself` };
1172
1325
  try {
1173
1326
  execFileSync3(bin, args, { cwd, stdio: ["ignore", "pipe", "pipe"], timeout: 12e4, encoding: "utf-8" });
@@ -1177,15 +1330,56 @@ function claude(args, cwd) {
1177
1330
  return { ok: false, reason: `${(e.stderr || e.stdout || e.message || "failed").trim().split("\n").slice(-1)[0]} (ran ${bin})` };
1178
1331
  }
1179
1332
  }
1180
- function turnOffSynced(id) {
1181
- const file = join11(claudeDir(), "settings.json");
1333
+ function setPluginIn(file, id, on) {
1182
1334
  const settings = existsSync10(file) ? readJson2(file) : {};
1183
1335
  if (!settings) return { ok: false, reason: `can't read ${file}` };
1184
1336
  const enabled = settings.enabledPlugins ?? {};
1185
- mkdirSync6(claudeDir(), { recursive: true });
1186
- writeFileSync6(file, JSON.stringify({ ...settings, enabledPlugins: { ...enabled, [id]: false } }, null, 2) + "\n");
1337
+ mkdirSync7(dirname3(file), { recursive: true });
1338
+ writeFileSync7(file, JSON.stringify({ ...settings, enabledPlugins: { ...enabled, [id]: on } }, null, 2) + "\n");
1187
1339
  return { ok: true };
1188
1340
  }
1341
+ function turnOffSynced(id) {
1342
+ return setPluginIn(join11(claudeDir(), "settings.json"), id, false);
1343
+ }
1344
+ function ignoreLocalSettings(root) {
1345
+ const file = ".claude/settings.local.json";
1346
+ const git2 = (...args) => execFileSync3("git", args, { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1347
+ try {
1348
+ git2("rev-parse", "--git-dir");
1349
+ } catch {
1350
+ return "";
1351
+ }
1352
+ try {
1353
+ git2("ls-files", "--error-unmatch", "--", file);
1354
+ return "; git tracks that file, so this shows in git status";
1355
+ } catch {
1356
+ }
1357
+ try {
1358
+ git2("check-ignore", "-q", "--", file);
1359
+ return ", which git ignores";
1360
+ } catch {
1361
+ }
1362
+ try {
1363
+ const exclude = resolve6(root, git2("rev-parse", "--git-path", "info/exclude"));
1364
+ const before = existsSync10(exclude) ? readFileSync8(exclude, "utf-8") : "";
1365
+ mkdirSync7(dirname3(exclude), { recursive: true });
1366
+ writeFileSync7(exclude, `${before}${before && !before.endsWith("\n") ? "\n" : ""}/${git2("rev-parse", "--show-prefix")}${file}
1367
+ `);
1368
+ return ", which git now ignores via .git/info/exclude";
1369
+ } catch {
1370
+ return "; git doesn't ignore that file yet: it's local only, don't commit it";
1371
+ }
1372
+ }
1373
+ function turnOffIn(plugin, root) {
1374
+ const done = `${plugin.id} turned off in ${basename4(root)}; other repos keep it`;
1375
+ const bin = plugin.synced ? null : claudeBinary();
1376
+ if (!bin) {
1377
+ const r2 = setPluginIn(join11(root, ".claude", "settings.local.json"), plugin.id, false);
1378
+ return r2.ok ? { ok: true, message: `${done} (in .claude/settings.local.json${ignoreLocalSettings(root)})` } : { ok: false, message: `${plugin.id}: ${r2.reason}` };
1379
+ }
1380
+ const r = claude(["plugin", "disable", plugin.id, "--scope", "local"], root, bin);
1381
+ return r.ok ? { ok: true, message: done } : { ok: false, message: `${plugin.id}: ${r.reason}` };
1382
+ }
1189
1383
  function removedFile() {
1190
1384
  return join11(skilllibHome(), "plugin-backup.json");
1191
1385
  }
@@ -1206,8 +1400,8 @@ function removePlugin(plugin, how) {
1206
1400
  }
1207
1401
  function recordRemovedPlugin(id, scope) {
1208
1402
  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");
1403
+ mkdirSync7(skilllibHome(), { recursive: true });
1404
+ writeFileSync7(removedFile(), JSON.stringify([...list2, { id, scope, at: (/* @__PURE__ */ new Date()).toISOString() }], null, 2) + "\n");
1211
1405
  }
1212
1406
  function pluginBackups() {
1213
1407
  return (readJson2(removedFile()) ?? []).map((r) => ({
@@ -1221,12 +1415,12 @@ function pluginBackups() {
1221
1415
  function restorePlugin(backup) {
1222
1416
  const r = claude(["plugin", "install", backup.from, "--scope", backup.path]);
1223
1417
  if (!r.ok) return r;
1224
- writeFileSync6(removedFile(), JSON.stringify((readJson2(removedFile()) ?? []).filter((x) => x.id !== backup.from), null, 2) + "\n");
1418
+ writeFileSync7(removedFile(), JSON.stringify((readJson2(removedFile()) ?? []).filter((x) => x.id !== backup.from), null, 2) + "\n");
1225
1419
  return { ok: true, to: `Claude Code plugins (${backup.path})` };
1226
1420
  }
1227
1421
  function cursorPluginSkills() {
1228
1422
  const cache = join11(userHome(), ".cursor", "plugins", "cache");
1229
- const dirs = (p) => existsSync10(p) ? readdirSync6(p).filter((d) => !d.startsWith(".") && statSync3(join11(p, d)).isDirectory()) : [];
1423
+ const dirs = (p) => existsSync10(p) ? readdirSync5(p).filter((d) => !d.startsWith(".") && statSync3(join11(p, d)).isDirectory()) : [];
1230
1424
  return dirs(cache).flatMap(
1231
1425
  (marketplace) => dirs(join11(cache, marketplace)).flatMap((plugin) => {
1232
1426
  const versions = dirs(join11(cache, marketplace, plugin)).map((v) => join11(cache, marketplace, plugin, v));
@@ -1237,8 +1431,8 @@ function cursorPluginSkills() {
1237
1431
  }
1238
1432
 
1239
1433
  // 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";
1434
+ import { existsSync as existsSync11, mkdirSync as mkdirSync8, readdirSync as readdirSync6, unlinkSync as unlinkSync3 } from "fs";
1435
+ import { basename as basename5, dirname as dirname4, join as join12, sep as sep4 } from "path";
1242
1436
  function entriesOf(dirs, name) {
1243
1437
  return dirs.flatMap((dir) => {
1244
1438
  const path = join12(dir, name);
@@ -1257,20 +1451,22 @@ function namesIn(dirs) {
1257
1451
  const names = /* @__PURE__ */ new Set();
1258
1452
  for (const dir of dirs) {
1259
1453
  if (!existsSync11(dir)) continue;
1260
- for (const name of readdirSync7(dir)) if (!name.startsWith(".") && name !== "synced") names.add(name);
1454
+ for (const name of readdirSync6(dir)) if (!name.startsWith(".") && name !== "synced") names.add(name);
1261
1455
  }
1262
1456
  return [...names].sort();
1263
1457
  }
1264
1458
  function runsFrom(id, dirsWithCopy, readsOf) {
1265
1459
  const reads = readsOf(id).filter((d) => dirsWithCopy.includes(d));
1266
- return reads.find((d) => d.endsWith(join12(".claude", "skills"))) ?? reads[0];
1460
+ if (id === "codex") return reads;
1461
+ const pick = (d) => CURSOR_PICKS.indexOf(basename5(dirname4(d)));
1462
+ return id === "cursor" ? [...reads].sort((a, b) => pick(a) - pick(b)).slice(0, 1) : reads.slice(0, 1);
1267
1463
  }
1268
1464
  function conflictOf(name, root, reals, enabled, readsOf) {
1269
1465
  const dirs = reals.map((r) => r.dir);
1270
1466
  return {
1271
1467
  name,
1272
1468
  root,
1273
- copies: reals.map((r) => ({ dir: r.dir, path: r.path, runs: enabled.filter((id) => runsFrom(id, dirs, readsOf) === r.dir) }))
1469
+ copies: reals.map((r) => ({ dir: r.dir, path: r.path, runs: enabled.filter((id) => runsFrom(id, dirs, readsOf).includes(r.dir)) }))
1274
1470
  };
1275
1471
  }
1276
1472
  function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {}) {
@@ -1297,7 +1493,7 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
1297
1493
  if (dep) {
1298
1494
  primary = reals.find((r) => rel(r.dir) === (dep.dir ?? PROJECT_SKILLS_DIR));
1299
1495
  if (!primary) continue;
1300
- } else if (chosen) {
1496
+ } else if (chosen && reals.some((r) => r.dir === chosen)) {
1301
1497
  primary = reals.find((r) => r.dir === chosen);
1302
1498
  } else {
1303
1499
  if (git2 === void 0) git2 = gitInfo(root);
@@ -1321,7 +1517,8 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
1321
1517
  if (!needed) steps.push({ kind: "unlink", path: e.path });
1322
1518
  } else steps.push(needed ? { kind: "link", path: e.path, target: primary.path } : { kind: "unlink", path: e.path });
1323
1519
  } else {
1324
- steps.push(needed ? { kind: "replace", path: e.path, target: primary.path } : { kind: "remove", path: e.path });
1520
+ const differs = e.hash !== primary.hash ? { differs: true } : {};
1521
+ steps.push(needed ? { kind: "replace", path: e.path, target: primary.path, ...differs } : { kind: "remove", path: e.path, ...differs });
1325
1522
  }
1326
1523
  }
1327
1524
  if (steps.length) report.plans.push({ name, root, primary: primary.path, steps });
@@ -1329,7 +1526,7 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
1329
1526
  return report;
1330
1527
  }
1331
1528
  function globalReadsOf(id) {
1332
- return HARNESSES.find((h) => h.id === id).globalDirs();
1529
+ return HARNESSES.find((h) => h.id === id).globalDirs().map(globalDirAs);
1333
1530
  }
1334
1531
  function isSkillsDirPlugin(path) {
1335
1532
  return existsSync11(join12(path, ".claude-plugin", "plugin.json"));
@@ -1344,15 +1541,16 @@ function planGlobalTidy({ keep = {}, enabled = enabledHarnesses() } = {}) {
1344
1541
  report.skipped.push({ name, reason: "a Claude Code plugin kept in a skills folder" });
1345
1542
  continue;
1346
1543
  }
1347
- const reals = entries.filter((e) => !e.link);
1544
+ const reals = entries.filter((e, i, all) => !e.link && all.findIndex((o) => !o.link && realpathOrNull2(o.path) === realpathOrNull2(e.path)) === i);
1348
1545
  if (reals.length < 2) continue;
1349
1546
  hashReals(reals);
1350
- const primary = reals.find((r) => r.dir === keep[name]) ?? reals.find((r) => r.dir === agents) ?? reals[0];
1351
- if (!keep[name] && reals.some((r) => r.hash !== primary.hash)) {
1547
+ const kept = reals.find((r) => r.dir === keep[name]);
1548
+ const primary = kept ?? reals.find((r) => r.dir === agents) ?? reals[0];
1549
+ if (!kept && reals.some((r) => r.hash !== primary.hash)) {
1352
1550
  report.conflicts.push(conflictOf(name, null, reals, enabled, globalReadsOf));
1353
1551
  continue;
1354
1552
  }
1355
- const steps = reals.filter((r) => r !== primary).map((r) => ({ kind: "replace", path: r.path, target: primary.path }));
1553
+ const steps = reals.filter((r) => r !== primary).map((r) => ({ kind: "replace", path: r.path, target: primary.path, ...r.hash !== primary.hash ? { differs: true } : {} }));
1356
1554
  report.plans.push({ name, root: null, primary: primary.path, steps });
1357
1555
  }
1358
1556
  return report;
@@ -1370,10 +1568,10 @@ function applyTidy(plan, { git: git2 = "keep", info: info2 } = {}) {
1370
1568
  for (const step of plan.steps) {
1371
1569
  if (held.includes(step) || !entryExists(step.path) || "target" in step && !entryExists(step.target)) continue;
1372
1570
  if (step.kind === "link" || step.kind === "unlink") {
1373
- if (isLink(step.path)) unlinkSync2(step.path);
1571
+ if (isLink(step.path)) unlinkSync3(step.path);
1374
1572
  } else stash(step.path, "tidy-backup");
1375
1573
  if (step.kind === "link" || step.kind === "replace") {
1376
- mkdirSync7(dirname4(step.path), { recursive: true });
1574
+ mkdirSync8(dirname4(step.path), { recursive: true });
1377
1575
  linkDir(step.target, step.path);
1378
1576
  }
1379
1577
  applied.push(step);
@@ -1390,20 +1588,27 @@ function applyTidy(plan, { git: git2 = "keep", info: info2 } = {}) {
1390
1588
  return { name: plan.name, root: plan.root, applied, held };
1391
1589
  }
1392
1590
  function describeStep(step, root) {
1393
- const shown = root ? relativeTo(root, step.path) : step.path.replace(userHome(), "~");
1591
+ const shown = root ? relativeTo(root, step.path) : tildify(step.path);
1394
1592
  switch (step.kind) {
1395
1593
  case "link":
1396
1594
  return `link ${shown}`;
1397
1595
  case "unlink":
1398
1596
  return `remove extra link ${shown}`;
1399
1597
  case "replace":
1400
- return `${shown}: duplicate copy \u2192 link`;
1598
+ return `${shown}: ${step.differs ? "differing" : "duplicate"} copy \u2192 link`;
1401
1599
  case "remove":
1402
- return `remove duplicate copy ${shown}`;
1600
+ return `remove ${step.differs ? "differing" : "duplicate"} copy ${shown}`;
1403
1601
  }
1404
1602
  }
1405
1603
  function copyLabel(conflict, dir) {
1406
- return conflict.root ? relativeTo(conflict.root, dir) : dir.replace(userHome(), "~");
1604
+ return folderLabel(conflict.root, dir);
1605
+ }
1606
+ function folderLabel(root, dir) {
1607
+ return root ? relativeTo(root, dir) : tildify(dir);
1608
+ }
1609
+ function foldersOf(root, name) {
1610
+ const entries = entriesOf(root ? ALL_PROJECT_DIRS.map((d) => join12(root, d)) : globalSkillDirs(), name);
1611
+ return { all: entries.map((e) => e.dir), real: entries.filter((e) => !e.link).map((e) => e.dir) };
1407
1612
  }
1408
1613
 
1409
1614
  // src/health.ts
@@ -1421,7 +1626,7 @@ function orThrow(r) {
1421
1626
  var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
1422
1627
  var agentNames = (ids) => ids.map((id) => HARNESSES.find((h) => h.id === id).name).join(", ");
1423
1628
  function conflictIssue(c) {
1424
- const where = c.root ? basename5(c.root) : "your global folders";
1629
+ const where = c.root ? basename6(c.root) : "your global folders";
1425
1630
  const winners = c.managed ? c.copies.filter((x) => x.dir === c.managed) : c.copies;
1426
1631
  return {
1427
1632
  id: `conflict:${c.root ?? "global"}:${c.name}`,
@@ -1444,20 +1649,22 @@ function applyAll(plans, git2) {
1444
1649
  const results = plans.map((p) => applyTidy(p, { git: git2 }));
1445
1650
  return { skills: results.filter((r) => r.applied.length).length, held: results.reduce((n, r) => n + r.held.length, 0) };
1446
1651
  }
1447
- function findIssues(projects2, machine, libraryNames, statusOf = projectStatus) {
1652
+ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus, loadedIn = (root) => skillsLoadedIn(root, machine), nestedOf = nestedSkills) {
1448
1653
  const issues = [];
1449
1654
  const statuses = new Map(projects2.map((root) => [root, statusOf(root)]));
1655
+ const nested = new Map(projects2.map((root) => [root, nestedOf(root)]));
1450
1656
  for (const skill of machine.filter((m) => m.broken)) {
1451
1657
  issues.push({
1452
1658
  id: `broken:${skill.name}`,
1453
1659
  severity: "problem",
1454
- title: `${skill.name}: broken link in ${skill.path.replace(/\/[^/]+$/, "").replace(/^\/Users\/[^/]+/, "~")}`,
1660
+ title: `${skill.name}: broken link in ${tildify(dirname5(skill.path))}`,
1455
1661
  detail: "It points at a folder that no longer exists, so it loads nothing.",
1456
1662
  fix: {
1457
1663
  label: "Remove the link",
1458
1664
  run: () => {
1459
1665
  const r = unloadGlobal(skill.path);
1460
- return r.ok ? `Removed broken link ${skill.name}` : `${skill.name}: ${r.reason}`;
1666
+ if (!r.ok) throw new Error(`${skill.name}: ${r.reason}`);
1667
+ return `Removed broken link ${skill.name}`;
1461
1668
  }
1462
1669
  }
1463
1670
  });
@@ -1474,40 +1681,96 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus)
1474
1681
  id: `dup:${name}`,
1475
1682
  severity: "problem",
1476
1683
  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)" : ""}.`
1684
+ 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
1685
  });
1479
1686
  }
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));
1484
- if (!dupes.length) continue;
1485
- const others = plugin.skills.length - dupes.length;
1486
- const pluginName = plugin.id.split("@")[0];
1487
- const remove2 = {
1488
- label: `Remove the plugin ${plugin.id}`,
1687
+ const loadedHere = new Map(projects2.map((root) => [root, loadedIn(root)]));
1688
+ const namesIn2 = (root, agent = "claude-code") => [
1689
+ ...new Set([...statuses.get(root), ...nested.get(root)].filter((s) => s.visibility.some((v) => v.id === agent && v.paths > 0)).map((s) => s.name))
1690
+ ];
1691
+ const pluginAgentsIn = (plugin, root) => new Set(loadedHere.get(root).filter((m) => plugin.skills.includes(m.path)).flatMap((m) => m.harnesses));
1692
+ const globalYours = new Set(loaded.filter((m) => m.movable).map((m) => m.name));
1693
+ const installedYours = /* @__PURE__ */ new Set([...globalYours, ...[...statuses.values()].flat().map((s) => s.name)]);
1694
+ const repoChoices = (plugin, root, names) => {
1695
+ const where = basename6(root);
1696
+ const copies = statuses.get(root).filter((s) => names.includes(s.name));
1697
+ const allManaged = names.every((n) => copies.some((c) => c.name === n && c.managed));
1698
+ return [
1699
+ { label: `Turn the plugin ${plugin.id} off in ${where} only`, hint: "other repos keep it; in .claude/settings.local.json, a local-only file", run: () => orThrow(turnOffIn(plugin, root)) },
1700
+ // Only copies skilllib installed: Your skills keeps them. A repo's own skill is the team's, never removed from here.
1701
+ ...names.length && allManaged ? [
1702
+ {
1703
+ label: `Remove your ${copies.map((s) => s.name).join(", ")} from ${where}`,
1704
+ hint: "the plugin's copy stays; Your skills keeps yours",
1705
+ run: () => {
1706
+ for (const s of copies) {
1707
+ const r = removeSkill(root, s.name);
1708
+ if (r.action !== "removed") throw new Error(`${s.name}: ${r.reason}`);
1709
+ }
1710
+ return `${copies.map((s) => s.name).join(", ")} removed from ${where}; the plugin's copy stays`;
1711
+ }
1712
+ }
1713
+ ] : []
1714
+ ];
1715
+ };
1716
+ const machinePlugins = claudePlugins(machine);
1717
+ for (const plugin of machinePlugins) {
1718
+ const names = plugin.skills.map((p) => basename6(p));
1719
+ const inRepos = projects2.filter((root) => pluginAgentsIn(plugin, root).has("claude-code")).map((root) => [root, namesIn2(root).filter((n) => names.includes(n))]).filter(([, d]) => d.length);
1720
+ const cursorRepos = enabledHarnesses().includes("claude-code") ? projects2.filter((root) => !pluginAgentsIn(plugin, root).has("claude-code") && pluginAgentsIn(plugin, root).has("cursor")).map((root) => [root, namesIn2(root, "cursor").filter((n) => names.includes(n))]).filter(([, d]) => d.length) : [];
1721
+ const claudeYours = /* @__PURE__ */ new Set([...globalYours, ...inRepos.flatMap(([, d]) => d)]);
1722
+ const loadedYours = /* @__PURE__ */ new Set([...claudeYours, ...cursorRepos.flatMap(([, d]) => d)]);
1723
+ const dupes = names.filter((n) => libraryNames.has(n) || loadedYours.has(n));
1724
+ if (!dupes.length) continue;
1725
+ const others = plugin.skills.length - dupes.length;
1726
+ const pluginName = plugin.id.split("@")[0];
1727
+ const remove2 = {
1728
+ label: `Remove the plugin ${plugin.id}`,
1489
1729
  hint: plugin.extras.length ? `also removes its ${plugin.extras.join(", ")}` : "restorable from Health",
1490
1730
  run: () => orThrow(removePlugin(plugin, "delete"))
1491
1731
  };
1492
1732
  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));
1733
+ const bothLoad = dupes.filter((n) => loadedYours.has(n));
1734
+ const claudeBoth = dupes.filter((n) => claudeYours.has(n));
1735
+ const cursorWheres = cursorRepos.map(([root]) => basename6(root));
1736
+ const notInstalled = dupes.filter((n) => !installedYours.has(n));
1494
1737
  issues.push({
1495
1738
  id: `plugin:${plugin.id}`,
1496
- severity: notLoaded.length === dupes.length ? "suggestion" : "problem",
1739
+ severity: bothLoad.length ? "problem" : "suggestion",
1497
1740
  title: `${dupes.join(", ")}: also in the Claude Code plugin ${plugin.id}`,
1498
1741
  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"}.` : "",
1501
- others ? `The plugin's other ${plural(others, "skill")} are copied into Your skills first, so nothing is lost.` : "",
1742
+ claudeBoth.length ? `Claude Code loads both (the plugin's as /${pluginName}:${claudeBoth[0]}). Your skill wins.` : "",
1743
+ cursorWheres.length ? `${cursorWheres.join(", ")} ${cursorWheres.length === 1 ? "turns" : "turn"} it off for Claude Code, but Cursor ignores repo settings and lists both there: only turning the plugin off or removing it changes that.` : "",
1744
+ notInstalled.length ? `Your ${notInstalled.join(", ")} ${notInstalled.length === 1 ? "is" : "are"} only in Your skills, not installed anywhere: once the plugin is gone, add ${notInstalled.length === 1 ? "it" : "them"} where you need ${notInstalled.length === 1 ? "it" : "them"}.` : "",
1745
+ others ? `The plugin's other ${others === 1 ? "skill is" : `${others} skills are`} copied into Your skills first, so nothing is lost.` : "",
1502
1746
  plugin.extras.length ? `It also brings ${plugin.extras.join(", ")}: ${plugin.synced ? "turning it off pauses those" : "removing it drops those, turning it off pauses them"}.` : "",
1503
- plugin.synced ? "It's synced from claude.ai, so it can only be turned off here." : ""
1747
+ plugin.synced ? "It's synced from claude.ai, so it can only be turned off here." : "",
1748
+ inRepos.length ? `Or keep it and turn it off only in ${inRepos.map(([root]) => basename6(root)).join(", ")}.` : ""
1504
1749
  ].filter(Boolean).join(" "),
1505
- choices: plugin.synced ? [off] : plugin.extras.length ? [off, remove2] : [remove2, off]
1750
+ choices: [...plugin.synced ? [off] : plugin.extras.length ? [off, remove2] : [remove2, off], ...inRepos.flatMap(([root, d]) => repoChoices(plugin, root, d))]
1506
1751
  });
1507
1752
  }
1753
+ const machinePluginSkills = new Set(machinePlugins.flatMap((p) => p.skills));
1754
+ for (const root of projects2) {
1755
+ const where = basename6(root);
1756
+ const repoOnly = loadedHere.get(root).filter((m) => m.kind === "plugin" && !machinePluginSkills.has(m.path));
1757
+ for (const plugin of claudePlugins(repoOnly, root)) {
1758
+ const names = plugin.skills.map((p) => basename6(p));
1759
+ const inRepo = namesIn2(root).filter((n) => names.includes(n));
1760
+ const dupes = names.filter((n) => inRepo.includes(n) || globalYours.has(n));
1761
+ if (!dupes.length) continue;
1762
+ issues.push({
1763
+ id: `plugin:${root}:${plugin.id}`,
1764
+ severity: "problem",
1765
+ title: `${dupes.join(", ")}: also in the Claude Code plugin ${plugin.id}, on in ${where}`,
1766
+ 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.`,
1767
+ choices: repoChoices(plugin, root, inRepo)
1768
+ });
1769
+ }
1770
+ }
1508
1771
  if (enabledHarnesses().includes("cursor")) {
1509
1772
  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]);
1773
+ for (const s of cursorPluginSkills().filter((x) => installedYours.has(x.name))) byPlugin.set(s.plugin, [...byPlugin.get(s.plugin) ?? [], s.name]);
1511
1774
  for (const [plugin, names] of byPlugin) {
1512
1775
  issues.push({
1513
1776
  id: `cursor-plugin:${plugin}`,
@@ -1526,39 +1789,121 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus)
1526
1789
  detail: `${global.plans.map((p) => p.name).join(", ")}: each keeps one copy (in ~/.agents/skills when there's one) and the others become links. Restorable from Health.`,
1527
1790
  fix: {
1528
1791
  label: "Keep one copy of each",
1529
- run: () => `${plural(applyAll(global.plans, "go").skills, "global skill")} now have one copy`
1792
+ run: () => {
1793
+ const r = applyAll(global.plans, "go");
1794
+ if (!r.skills) throw new Error("No global skill changed");
1795
+ return `${plural(r.skills, "global skill")} now ${r.skills === 1 ? "has" : "have"} one copy`;
1796
+ }
1530
1797
  }
1531
1798
  });
1532
1799
  }
1533
1800
  issues.push(...global.conflicts.map(conflictIssue));
1534
1801
  const kept = keptGlobal();
1802
+ const unreviewed = [...new Set(loaded.filter((m) => m.movable && !kept.has(m.name)).map((m) => m.name))];
1803
+ if (unreviewed.length) {
1804
+ issues.push({
1805
+ id: "review:global",
1806
+ severity: "suggestion",
1807
+ title: `${plural(unreviewed.length, "global skill")} of yours ${unreviewed.length === 1 ? "loads" : "load"} in every repo`,
1808
+ detail: `${unreviewed.join(", ")}. Move each to the repos that need it (skilllib \u2192 Global \u2192 Move it to the repos that need it\u2026), or mark the ones you want everywhere as global on purpose (skilllib global keep <name>).`
1809
+ });
1810
+ }
1811
+ const enabled = enabledHarnesses();
1812
+ const seenBy = /* @__PURE__ */ new Map();
1813
+ for (const m of loaded) seenBy.set(m.name, /* @__PURE__ */ new Set([...seenBy.get(m.name) ?? [], ...m.harnesses]));
1814
+ const blindOf = (m) => enabled.filter((id) => !seenBy.get(m.name).has(id));
1815
+ const blindGlobal = loaded.filter((m) => m.movable && m.harnesses.length && blindOf(m).length);
1816
+ for (const onPurpose of [true, false]) {
1817
+ const skills = blindGlobal.filter((m) => kept.has(m.name) === onPurpose);
1818
+ if (!skills.length) continue;
1819
+ const missing = [...new Set(skills.flatMap(blindOf))];
1820
+ const link2 = {
1821
+ label: `Link ${skills.length === 1 ? "it" : "them"} for ${agentNames(missing)}`,
1822
+ hint: "links only; nothing is copied or moved",
1823
+ run: () => {
1824
+ const results = skills.map((m) => ({ m, ...linkGlobal(m.path, blindOf(m)) }));
1825
+ const skipped = results.flatMap((r) => r.skipped);
1826
+ if (!results.some((r) => r.linked.length)) throw new Error(`Nothing linked${skipped.length ? `; skipped ${skipped.join(", ")} (a different skill has that name)` : ""}`);
1827
+ return `Linked ${plural(results.filter((r) => r.linked.length).length, "global skill")} for ${agentNames(missing)}${skipped.length ? `; skipped ${skipped.join(", ")} (a different skill has that name)` : ""}`;
1828
+ }
1829
+ };
1830
+ issues.push({
1831
+ id: `usable:global${onPurpose ? ":kept" : ""}`,
1832
+ severity: "suggestion",
1833
+ title: `${plural(skills.length, "global skill")}${onPurpose ? " you keep global" : ""} not usable by ${agentNames(missing)}`,
1834
+ 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.`}`,
1835
+ ...onPurpose ? { fix: link2 } : { choices: [link2] }
1836
+ });
1837
+ }
1535
1838
  const alsoGlobal = /* @__PURE__ */ new Map();
1536
- const globalNames = new Set(loaded.filter((m) => m.kind !== "plugin").map((m) => m.name));
1839
+ const globalCopies = loaded.filter((m) => m.kind !== "plugin");
1840
+ 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
1841
  for (const root of projects2) {
1538
- const where = basename5(root);
1842
+ const where = basename6(root);
1539
1843
  const status2 = statuses.get(root);
1540
- const behind = status2.filter((s) => s.state === "update available" || s.state === "folder missing");
1541
- if (behind.length > 0) {
1844
+ const restorable = status2.filter((s) => s.state === "folder missing" && s.latest !== null);
1845
+ if (restorable.length > 0) {
1542
1846
  issues.push({
1543
1847
  id: `sync:${root}`,
1544
1848
  severity: "problem",
1545
- title: `${where}: ${plural(behind.length, "skill")} out of date`,
1546
- detail: behind.map((s) => s.name).join(", "),
1849
+ title: `${where}: ${plural(restorable.length, "skill")} in skilllib.json not installed`,
1850
+ detail: `${restorable.map((s) => s.name).join(", ")}: the folder is missing`,
1547
1851
  fix: {
1548
1852
  label: "Sync the project",
1549
1853
  run: () => {
1550
1854
  const changes = syncProject(root);
1551
- return `${where}: ${changes.filter((c) => c.action !== "skipped").length} updated`;
1855
+ const done = changes.filter((c) => c.action !== "skipped");
1856
+ const skipped = changes.filter((c) => c.action === "skipped").map((c) => `${c.name}: ${c.reason}`);
1857
+ if (!done.length) throw new Error(`${where}: nothing restored${skipped.length ? `; ${skipped.join("; ")}` : ""}`);
1858
+ return `${where}: restored ${plural(done.length, "skill")}${skipped.length ? `; skipped ${skipped.join("; ")}` : ""}`;
1552
1859
  }
1553
1860
  }
1554
1861
  });
1555
1862
  }
1863
+ for (const s of status2.filter((s2) => s2.state === "folder missing" && s2.latest === null))
1864
+ issues.push({
1865
+ id: `unavailable:${root}:${s.name}`,
1866
+ severity: "problem",
1867
+ title: `${s.name}: pinned in ${where}'s skilllib.json, but not in your library`,
1868
+ detail: `Its folder is missing, and your library (${tildify(libraryDir())}) has no copy to restore: import it, or set SKILLLIB_HOME to the library that has it.`,
1869
+ // Editing skilllib.json is the team's call: a choice, never run by `doctor --fix`.
1870
+ choices: [
1871
+ {
1872
+ label: "Remove it from skilllib.json",
1873
+ hint: `${where} stops pinning it`,
1874
+ run: () => {
1875
+ const r = removeSkill(root, s.name);
1876
+ if (r.action !== "removed") throw new Error(`${s.name}: ${r.reason}`);
1877
+ return `${s.name}: removed from ${where}'s skilllib.json`;
1878
+ }
1879
+ }
1880
+ ]
1881
+ });
1882
+ const behind = status2.filter((s) => s.state === "update available");
1883
+ if (behind.length > 0)
1884
+ issues.push({
1885
+ id: `update:${root}`,
1886
+ severity: "suggestion",
1887
+ title: `${where}: ${plural(behind.length, "skill")} out of date`,
1888
+ detail: behind.map((s) => `${s.name} (v${s.version} \u2192 v${s.latest})`).join(", "),
1889
+ fix: {
1890
+ label: "Update to the newest versions",
1891
+ run: () => {
1892
+ const changes = updateProject(root, behind.map((s) => s.name));
1893
+ const done = changes.filter((c) => c.action !== "skipped");
1894
+ const skipped = changes.filter((c) => c.action === "skipped").map((c) => `${c.name}: ${c.reason}`);
1895
+ if (!done.length) throw new Error(`${where}: nothing updated${skipped.length ? `; ${skipped.join("; ")}` : ""}`);
1896
+ return `${where}: updated ${plural(done.length, "skill")}${skipped.length ? `; skipped ${skipped.join("; ")}` : ""}`;
1897
+ }
1898
+ }
1899
+ });
1556
1900
  const tidy2 = planProjectTidy(root);
1557
1901
  if (tidy2.plans.length) {
1558
- const git2 = gitInfo(root);
1559
- const visible2 = tidy2.plans.flatMap((p) => gitVisibleSteps(p, git2).map((s) => describeStep(s, root)));
1902
+ const git3 = gitInfo(root);
1903
+ const visible2 = tidy2.plans.flatMap((p) => gitVisibleSteps(p, git3).map((s) => describeStep(s, root)));
1560
1904
  const run = (mode) => () => {
1561
1905
  const r = applyAll(tidy2.plans, mode);
1906
+ if (!r.skills) throw new Error(`${where}: nothing tidied${r.held ? `; left ${plural(r.held, "change")} git would see` : ""}`);
1562
1907
  return `${where}: tidied ${plural(r.skills, "skill")}${r.held ? `; left ${plural(r.held, "change")} git would see` : ""}`;
1563
1908
  };
1564
1909
  issues.push({
@@ -1575,14 +1920,24 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus)
1575
1920
  });
1576
1921
  }
1577
1922
  issues.push(...tidy2.conflicts.map(conflictIssue));
1578
- const partial = status2.filter((s) => s.state !== "folder missing" && s.visibility.some((v) => v.paths === 0));
1923
+ const blindHere = status2.filter((s) => s.state !== "folder missing" && s.visibility.some((v) => v.paths === 0)).map((s) => ({ s, would: linkEverywhere(root, s.name, s.location, { dryRun: true }) }));
1924
+ for (const { s, would } of blindHere.filter(({ would: w }) => !w.created.length && !w.blocked.length && w.uncommitted.length))
1925
+ issues.push({
1926
+ id: `uncommitted:${root}:${s.name}`,
1927
+ severity: "suggestion",
1928
+ title: `${s.name} in ${where}: ${agentNames(s.visibility.filter((v) => v.paths === 0).map((v) => v.id))} can't use it until it's committed`,
1929
+ detail: `Git tracks ${would.uncommitted.join(", ")}, but not ${s.location}/${s.name}: a link there would be broken for teammates. Commit it, then link it (\`skilllib link --allow-tracked\`, or from Health).`
1930
+ });
1931
+ const partial = blindHere.filter(({ would }) => would.created.length || would.blocked.length).map(({ s }) => s);
1579
1932
  if (partial.length) {
1580
1933
  const missing = [...new Set(partial.flatMap((s) => s.visibility.filter((v) => v.paths === 0).map((v) => v.id)))];
1581
- const tracked = readConfig().agentsDirOk?.includes(root) ? [] : [...new Set(missing.map((id) => harness(id).projectDirs[0]))].filter((dir) => isGitTracked(root, dir));
1934
+ const tracked = [...new Set(blindHere.flatMap(({ would }) => would.blocked))];
1582
1935
  const run = (allowTracked) => () => {
1583
1936
  if (allowTracked) allowTrackedLinks(root);
1584
1937
  const r = linkAll(root, { allowTracked });
1585
- return `${where}: linked ${plural(r.linked.length, "skill")} for ${agentNames(missing)}${r.blocked.length ? `; skipped ${r.blocked.join(", ")} (git tracks it)` : ""}`;
1938
+ const notes = `${r.blocked.length ? `; skipped ${r.blocked.join(", ")} (git tracks ${r.blocked.length === 1 ? "it" : "them"})` : ""}${r.uncommitted.length ? `; ${r.uncommitted.join(", ")} not linked into folders git tracks (not committed, so teammates would get broken links)` : ""}`;
1939
+ if (!r.linked.length) throw new Error(`${where}: nothing linked${notes}`);
1940
+ return `${where}: linked ${plural(r.linked.length, "skill")} for ${agentNames(missing)}${notes}`;
1586
1941
  };
1587
1942
  issues.push({
1588
1943
  id: `usable:${root}`,
@@ -1591,20 +1946,40 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus)
1591
1946
  detail: `${partial.map((s) => `${s.name} (not ${agentNames(s.visibility.filter((v) => v.paths === 0).map((v) => v.id))})`).join(", ")}. Adding links makes the one real copy reach every agent you use; nothing is copied or moved.${tracked.length ? ` Git tracks ${tracked.join(", ")}, so links there show in git status.` : ""}`,
1592
1947
  ...tracked.length ? {
1593
1948
  choices: [
1594
- { label: "Add links, but not where git tracks the folder", hint: `skips ${tracked.join(", ")}`, run: run(false) },
1949
+ // Only when some link goes where git doesn't track: otherwise this one would link nothing.
1950
+ ...blindHere.some(({ would }) => would.created.length) ? [{ label: "Add links, but not where git tracks the folder", hint: `skips ${tracked.join(", ")}`, run: run(false) }] : [],
1595
1951
  { label: "Add links everywhere", hint: `remembered for ${where}; git will see them`, run: run(true) }
1596
1952
  ]
1597
1953
  } : { fix: { label: "Add the links", run: run(false) } }
1598
1954
  });
1599
1955
  }
1600
1956
  const differing = new Set(tidy2.conflicts.map((c) => c.name));
1957
+ const git2 = gitInfo(root);
1958
+ const committed = (s) => ["committed", "changed"].includes(git2?.of(relativeTo(root, s.path)) ?? "");
1601
1959
  for (const s of status2) {
1602
1960
  if (isLink(s.path) || differing.has(s.name)) continue;
1603
- if (s.state === "local only") {
1961
+ if (s.state === "local only" && committed(s)) {
1604
1962
  issues.push({
1605
1963
  id: `local:${root}:${s.name}`,
1606
1964
  severity: "suggestion",
1607
- title: `${s.name}: only exists in ${where}`,
1965
+ title: `${s.name} in ${where} isn't in your library`,
1966
+ detail: "Copy it into the library to reuse it in other projects. The repo's copy stays as it is: it's the team's (committed).",
1967
+ choices: [
1968
+ {
1969
+ label: "Copy into the library",
1970
+ hint: `${where}'s copy stays as it is`,
1971
+ run: () => {
1972
+ importSkill(s.path);
1973
+ return `${s.name} copied into your library; ${where}'s copy stays as it is`;
1974
+ }
1975
+ }
1976
+ ]
1977
+ });
1978
+ } else if (s.state === "local only") {
1979
+ issues.push({
1980
+ id: `local:${root}:${s.name}`,
1981
+ severity: "suggestion",
1982
+ title: `${s.name} in ${where} isn't in your library`,
1608
1983
  detail: "Import it into the library to reuse it in other projects and keep it backed up. The repo then tracks it in skilllib.json.",
1609
1984
  choices: [
1610
1985
  {
@@ -1627,27 +2002,67 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus)
1627
2002
  choices: [{ label: "Track it", hint: `adds it to ${where}'s skilllib.json`, run: () => `${addSkill(root, s.name).name} tracked in ${where}` }]
1628
2003
  });
1629
2004
  }
1630
- if (globalNames.has(s.name) && s.managed) alsoGlobal.set(s.name, [...alsoGlobal.get(s.name) ?? [], root]);
2005
+ if (s.state === "edited locally" || s.state === "edited locally, update available") {
2006
+ const newer = s.state !== "edited locally";
2007
+ const pinned = s.version ? `v${s.version}` : "the library version";
2008
+ issues.push({
2009
+ id: `edited:${root}:${s.name}`,
2010
+ severity: "suggestion",
2011
+ title: `${s.name} in ${where} has local edits`,
2012
+ 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.`,
2013
+ choices: [
2014
+ {
2015
+ label: "Save as a new version in Your skills",
2016
+ 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`,
2017
+ run: () => {
2018
+ importSkill(s.path, { force: true });
2019
+ const c = addSkill(root, s.name);
2020
+ if (c.action === "skipped") throw new Error(`${s.name}: ${c.reason}`);
2021
+ return `${s.name}: saved as v${c.to}; ${where} uses it`;
2022
+ }
2023
+ },
2024
+ {
2025
+ label: "Discard the edits",
2026
+ hint: `back to ${pinned}; the edited copy is restorable from Health`,
2027
+ run: () => {
2028
+ const c = discardEdits(root, s.name);
2029
+ if (c.action === "skipped") throw new Error(`${s.name}: ${c.reason}`);
2030
+ return `${s.name}: edits discarded, back to v${c.to}`;
2031
+ }
2032
+ }
2033
+ ]
2034
+ });
2035
+ }
2036
+ if (alsoLoadedGlobally(s)) alsoGlobal.set(s.name, [...alsoGlobal.get(s.name) ?? [], { root, managed: s.managed }]);
1631
2037
  }
1632
2038
  }
1633
- for (const [name, roots] of alsoGlobal) {
1634
- const wheres = roots.map((r) => basename5(r)).join(", ");
2039
+ for (const root of projects2) {
2040
+ for (const n of nested.get(root)) {
2041
+ const copies = alsoGlobal.get(n.name) ?? [];
2042
+ if (alsoLoadedGlobally(n) && !copies.some((c) => c.root === root)) alsoGlobal.set(n.name, [...copies, { root, managed: false }]);
2043
+ }
2044
+ }
2045
+ for (const [name, copies] of alsoGlobal) {
2046
+ const wheres = copies.map((c) => basename6(c.root)).join(", ");
2047
+ const removable = copies.filter((c) => c.managed).map((c) => c.root);
2048
+ const repoOwn = copies.filter((c) => !c.managed).map((c) => basename6(c.root));
1635
2049
  if (kept.has(name)) {
2050
+ const removeWheres = removable.map((r) => basename6(r)).join(", ");
1636
2051
  issues.push({
1637
2052
  id: `twice:${name}`,
1638
2053
  severity: "suggestion",
1639
2054
  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: [
2055
+ 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.` : ""}`,
2056
+ choices: removable.length ? [
1642
2057
  {
1643
- label: `Remove the copies in ${wheres}`,
2058
+ label: `Remove the copies in ${removeWheres}`,
1644
2059
  hint: "the global copy stays",
1645
2060
  run: () => {
1646
- const removed = roots.filter((root) => removeSkill(root, name).action === "removed").map((r) => basename5(r));
2061
+ const removed = removable.filter((root) => removeSkill(root, name).action === "removed").map((r) => basename6(r));
1647
2062
  return `${name}: removed from ${removed.join(", ") || "no project"}; the global copy stays`;
1648
2063
  }
1649
2064
  }
1650
- ]
2065
+ ] : void 0
1651
2066
  });
1652
2067
  continue;
1653
2068
  }
@@ -1656,7 +2071,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus)
1656
2071
  id: `twice:${name}`,
1657
2072
  severity: "suggestion",
1658
2073
  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).",
2074
+ 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
2075
  choices: movable ? [
1661
2076
  {
1662
2077
  label: "Stop loading it globally",
@@ -1671,8 +2086,311 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus)
1671
2086
  ] : void 0
1672
2087
  });
1673
2088
  }
2089
+ const agentSkill2 = agentSkillState();
2090
+ if (agentSkill2 !== "installed") {
2091
+ const install = {
2092
+ label: agentSkill2 === "missing" ? "Install the skilllib skill for your agents" : "Update the skilllib skill",
2093
+ hint: "one global skill, in ~/.claude/skills (and ~/.agents/skills)",
2094
+ run: () => {
2095
+ const r = installAgentSkill();
2096
+ if (!r.ok) throw new Error(`skilllib skill: ${r.reason}`);
2097
+ return `Your agents can now use skilllib (${plural(r.dirs.length, "folder")})`;
2098
+ }
2099
+ };
2100
+ issues.push({
2101
+ id: "agent-skill",
2102
+ severity: "suggestion",
2103
+ title: agentSkill2 === "missing" ? "Your agents don't know about skilllib" : "The skilllib skill for your agents is out of date",
2104
+ 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.",
2105
+ ...agentSkill2 === "missing" ? { choices: [install] } : { fix: install }
2106
+ });
2107
+ }
1674
2108
  return issues;
1675
2109
  }
2110
+ function addedAt(path) {
2111
+ try {
2112
+ const st = lstatSync4(path);
2113
+ return st.birthtimeMs > 0 ? st.birthtimeMs : st.ctimeMs;
2114
+ } catch {
2115
+ return Date.now();
2116
+ }
2117
+ }
2118
+ function usageIssues(projects2, machine, libraryNames, uses, statusOf = projectStatus) {
2119
+ if (!enabledHarnesses().includes("claude-code")) return [];
2120
+ const issues = [];
2121
+ const since = Date.now() - uses.days * 24 * 60 * 60 * 1e3;
2122
+ for (const root of projects2) {
2123
+ const where = basename6(root);
2124
+ const unused = statusOf(root).filter((s) => s.state !== "folder missing" && uses.inProject(root, s.name) === 0 && addedAt(s.path) <= since);
2125
+ if (!unused.length) continue;
2126
+ const removable = unused.filter((s) => s.managed);
2127
+ issues.push({
2128
+ id: `unused:${root}`,
2129
+ severity: "suggestion",
2130
+ title: `${where}: ${plural(unused.length, "skill")} unused in ${uses.days} days`,
2131
+ 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." : ""}`,
2132
+ choices: removable.length ? [
2133
+ {
2134
+ label: `Remove ${removable.length === unused.length ? "them" : `the ${plural(removable.length, "skill")} skilllib installed`} from ${where}`,
2135
+ hint: "Your skills keeps them; edited ones stay",
2136
+ run: () => {
2137
+ const changes = removable.map((s) => removeSkill(root, s.name));
2138
+ const removed = changes.filter((c) => c.action === "removed").map((c) => c.name);
2139
+ const kept = changes.filter((c) => c.action !== "removed").map((c) => c.name);
2140
+ return `${where}: removed ${removed.join(", ") || "nothing"}${kept.length ? `; kept ${kept.join(", ")} (local edits)` : ""}`;
2141
+ }
2142
+ }
2143
+ ] : void 0
2144
+ });
2145
+ }
2146
+ const installed = /* @__PURE__ */ new Set([...projects2.flatMap((root) => statusOf(root).map((s) => s.name)), ...machine.filter((m) => !m.broken).map((m) => m.name)]);
2147
+ const idle = [...libraryNames].filter((name) => {
2148
+ const first = versionHistory(name)[0];
2149
+ return !installed.has(name) && uses.total(name) === 0 && (first ? Date.parse(first.date) : addedAt(librarySkillDir(name))) <= since;
2150
+ });
2151
+ if (idle.length) {
2152
+ issues.push({
2153
+ id: "unused:library",
2154
+ severity: "suggestion",
2155
+ title: `${plural(idle.length, "skill")} in Your skills: in no repo, unused in ${uses.days} days`,
2156
+ 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.`,
2157
+ choices: [
2158
+ {
2159
+ label: `Delete ${idle.length === 1 ? "it" : `all ${idle.length}`} from Your skills`,
2160
+ hint: "restorable from Health",
2161
+ run: () => {
2162
+ const done = idle.map((name) => [name, deleteLibrarySkill(name)]);
2163
+ const failed = done.filter(([, r]) => !r.ok).map(([name]) => name);
2164
+ return `Deleted ${plural(done.length - failed.length, "skill")} from Your skills${failed.length ? `; kept ${failed.join(", ")}` : ""}`;
2165
+ }
2166
+ }
2167
+ ]
2168
+ });
2169
+ }
2170
+ return issues;
2171
+ }
2172
+
2173
+ // src/here.ts
2174
+ function groupBy(items) {
2175
+ const groups = /* @__PURE__ */ new Map();
2176
+ for (const [key, name, make] of items) {
2177
+ const group = groups.get(key) ?? make();
2178
+ group.skills.push(name);
2179
+ groups.set(key, group);
2180
+ }
2181
+ return [...groups.values()];
2182
+ }
2183
+ var sameSet = (a, b) => a.length === b.length && a.every((x) => b.includes(x));
2184
+ function globalFrom(kind, origin, path) {
2185
+ if (kind === "global" || kind === "skilllib") return tildify(path.replace(/[\\/][^\\/]+$/, "")).replace(/\\/g, "/");
2186
+ return origin;
2187
+ }
2188
+ var SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "target", "vendor", "coverage"]);
2189
+ function topLevel(root) {
2190
+ try {
2191
+ 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);
2192
+ } catch {
2193
+ return [];
2194
+ }
2195
+ }
2196
+ function usableHere(root, uses = /* @__PURE__ */ new Map()) {
2197
+ const agents = enabledHarnesses();
2198
+ const inRepo = projectHere(root) === root;
2199
+ const nestedHere = inRepo ? nestedSkills(root, { enabled: agents }) : [];
2200
+ const git2 = gitInfo(root, [...new Set(nestedHere.map((s) => s.location))]);
2201
+ const group = (list2) => groupBy(
2202
+ list2.map((s) => {
2203
+ const loadedBy = s.visibility.filter((v) => v.paths > 0).map((v) => v.id);
2204
+ const behind = s.managed && s.latest !== null && s.version !== null && s.latest > s.version;
2205
+ const gitState = git2?.of(relativeTo(root, s.path));
2206
+ const repo = !s.managed && (s.state.startsWith("repo skill") || gitState === "committed" || gitState === "changed");
2207
+ const usual = s.state === "ok" || s.state === "local only" || repo && s.state !== "from npx skills";
2208
+ const attrs = {
2209
+ source: s.managed ? "library" : repo ? "repo" : "local",
2210
+ ...s.location !== ".claude/skills" && { dir: s.location },
2211
+ ...s.version !== null && { version: s.version },
2212
+ ...!usual && { state: s.state },
2213
+ ...behind && { latest: s.latest },
2214
+ ...gitState && { git: gitState },
2215
+ ...!sameSet(loadedBy, agents) && { agents: loadedBy }
2216
+ };
2217
+ return [JSON.stringify(attrs), s.name, () => ({ ...attrs, skills: [] })];
2218
+ })
2219
+ );
2220
+ const skills = group(inRepo ? projectStatus(root) : []);
2221
+ const nested = group(nestedHere);
2222
+ const machine = machineSkills(agents);
2223
+ const loaded = inRepo ? skillsLoadedIn(root, machine, agents) : machine;
2224
+ const global = groupBy(
2225
+ loaded.filter((m) => !m.broken && m.harnesses.length > 0).map((m) => {
2226
+ const from = globalFrom(m.kind, m.origin, m.path);
2227
+ return [`${m.kind}\0${from}\0${m.harnesses.join()}`, m.name, () => ({ source: m.kind, from, agents: m.harnesses, skills: [] })];
2228
+ })
2229
+ );
2230
+ const issues = /* @__PURE__ */ new Map();
2231
+ for (const i of findIssues(inRepo ? [root] : [], machine, new Set(librarySkills().map((l) => l.name)))) {
2232
+ const choices = i.choices?.map((c) => c.label);
2233
+ const key = `${i.detail}\0${i.fix?.label ?? ""}\0${choices?.join() ?? ""}`;
2234
+ const issue = issues.get(key) ?? { problems: [], detail: i.detail, ...i.fix && { fix: i.fix.label }, ...choices && { choices } };
2235
+ issue.problems.push(i.title);
2236
+ issues.set(key, issue);
2237
+ }
2238
+ const usesHere = Object.fromEntries([...uses].filter(([, n]) => n > 0));
2239
+ return {
2240
+ project: tildify(root),
2241
+ files: topLevel(root),
2242
+ agents,
2243
+ skills,
2244
+ ...nested.length > 0 && { nested },
2245
+ global,
2246
+ issues: [...issues.values()],
2247
+ ...Object.keys(usesHere).length > 0 && { usesHere }
2248
+ };
2249
+ }
2250
+ function firstSentence(description) {
2251
+ const line = description.replace(/\s+/g, " ").trim();
2252
+ const sentence = line.match(/^(.{40,}?[.!?])(?:\s|$)/)?.[1] ?? line;
2253
+ return sentence.length > 200 ? sentence.slice(0, 199).trimEnd() + "\u2026" : sentence;
2254
+ }
2255
+ function libraryFor(currentProject) {
2256
+ const manifests = visibleProjects().map((p) => ({ name: basename7(p), skills: readManifest(p).skills }));
2257
+ const here = currentProject ? new Set(projectStatus(currentProject).map((s) => s.name)) : null;
2258
+ const all = librarySkills();
2259
+ const describe = (s) => {
2260
+ const projects2 = manifests.filter((m) => s.name in m.skills).map((m) => m.name);
2261
+ return { name: s.name, description: firstSentence(s.description), ...projects2.length > 0 && { projects: projects2 } };
2262
+ };
2263
+ if (!here) return { skills: all.map(describe) };
2264
+ return { inThisProject: all.filter((s) => here.has(s.name)).map((s) => s.name), skills: all.filter((s) => !here.has(s.name)).map(describe) };
2265
+ }
2266
+
2267
+ // src/usage.ts
2268
+ import { createReadStream, existsSync as existsSync12, readdirSync as readdirSync8, statSync as statSync4 } from "fs";
2269
+ import { join as join13 } from "path";
2270
+ import { createInterface } from "readline";
2271
+ var BUILTIN_COMMANDS = /* @__PURE__ */ new Set([
2272
+ "add-dir",
2273
+ "agents",
2274
+ "bug",
2275
+ "clear",
2276
+ "compact",
2277
+ "config",
2278
+ "context",
2279
+ "cost",
2280
+ "doctor",
2281
+ "exit",
2282
+ "export",
2283
+ "fast",
2284
+ "feedback",
2285
+ "goal",
2286
+ "help",
2287
+ "hooks",
2288
+ "ide",
2289
+ "init",
2290
+ "install-github-app",
2291
+ "login",
2292
+ "logout",
2293
+ "mcp",
2294
+ "memory",
2295
+ "model",
2296
+ "output-style",
2297
+ "permissions",
2298
+ "plugin",
2299
+ "pr-comments",
2300
+ "privacy-settings",
2301
+ "release-notes",
2302
+ "resume",
2303
+ "rewind",
2304
+ "status",
2305
+ "statusline",
2306
+ "terminal-setup",
2307
+ "todos",
2308
+ "upgrade",
2309
+ "usage",
2310
+ "vim"
2311
+ ]);
2312
+ function skillNameFromPath(filePath) {
2313
+ const match = filePath.replace(/\\/g, "/").match(/\/\.(?:claude|agents)\/skills\/([^/]+)\//);
2314
+ return match ? match[1] : null;
2315
+ }
2316
+ function usesFromTranscriptLine(line) {
2317
+ if (!line.includes('"Skill"') && !line.includes("<command-name>") && !line.includes("/skills/")) return [];
2318
+ let record;
2319
+ try {
2320
+ record = JSON.parse(line);
2321
+ } catch {
2322
+ return [];
2323
+ }
2324
+ const cwd = record.cwd ?? "";
2325
+ const at = record.timestamp ?? "";
2326
+ const content = record.message?.content;
2327
+ if (record.type === "assistant" && Array.isArray(content)) {
2328
+ return content.flatMap((block) => {
2329
+ if (!block || typeof block !== "object" || block.type !== "tool_use") return [];
2330
+ const key = String(block.id);
2331
+ if (block.name === "Skill" && typeof block.input?.skill === "string") {
2332
+ return [{ skill: block.input.skill, source: "invoked", cwd, at, key }];
2333
+ }
2334
+ if (block.name === "Read" && typeof block.input?.file_path === "string") {
2335
+ const skill = skillNameFromPath(block.input.file_path);
2336
+ return skill ? [{ skill, source: "file read", cwd, at, key }] : [];
2337
+ }
2338
+ return [];
2339
+ });
2340
+ }
2341
+ if (record.type === "user" && typeof content === "string") {
2342
+ const match = content.match(/<command-name>\/?([\w.:-]+)<\/command-name>/);
2343
+ if (!match || BUILTIN_COMMANDS.has(match[1])) return [];
2344
+ return [{ skill: match[1], source: "slash", cwd, at, key: record.uuid ?? `${at}:${match[1]}` }];
2345
+ }
2346
+ return [];
2347
+ }
2348
+ function transcriptFiles(sinceMs) {
2349
+ const base = join13(claudeDir(), "projects");
2350
+ if (!existsSync12(base)) return [];
2351
+ return readdirSync8(base).flatMap((project) => {
2352
+ const dir = join13(base, project);
2353
+ if (!statSync4(dir).isDirectory()) return [];
2354
+ return readdirSync8(dir).filter((f) => f.endsWith(".jsonl")).map((f) => join13(dir, f)).filter((f) => statSync4(f).mtimeMs >= sinceMs);
2355
+ });
2356
+ }
2357
+ async function scanUsage(days) {
2358
+ const sinceMs = Date.now() - days * 24 * 60 * 60 * 1e3;
2359
+ const seen = /* @__PURE__ */ new Map();
2360
+ for (const file of transcriptFiles(sinceMs)) {
2361
+ const lines = createInterface({ input: createReadStream(file), crlfDelay: Infinity });
2362
+ for await (const line of lines) {
2363
+ for (const use of usesFromTranscriptLine(line)) {
2364
+ if (Date.parse(use.at) >= sinceMs) seen.set(use.key, use);
2365
+ }
2366
+ }
2367
+ }
2368
+ return [...seen.values()];
2369
+ }
2370
+ function summarize(uses, projectOf = () => null) {
2371
+ const bySkill = /* @__PURE__ */ new Map();
2372
+ for (const use of uses) {
2373
+ const skill = use.skill.split(":").pop();
2374
+ const summary = bySkill.get(skill) ?? { skill, uses: 0, lastUsed: "", projects: /* @__PURE__ */ new Set() };
2375
+ summary.uses += 1;
2376
+ if (use.at > summary.lastUsed) summary.lastUsed = use.at;
2377
+ const project = projectOf(use.cwd);
2378
+ if (project) summary.projects.add(project);
2379
+ bySkill.set(skill, summary);
2380
+ }
2381
+ return [...bySkill.values()].sort((a, b) => b.uses - a.uses);
2382
+ }
2383
+ function usesByProject(uses, projectOf) {
2384
+ const byProject = /* @__PURE__ */ new Map();
2385
+ for (const use of uses) {
2386
+ const root = projectOf(use.cwd);
2387
+ if (!root) continue;
2388
+ const counts = byProject.get(root) ?? /* @__PURE__ */ new Map();
2389
+ counts.set(use.skill, (counts.get(use.skill) ?? 0) + 1);
2390
+ byProject.set(root, counts);
2391
+ }
2392
+ return byProject;
2393
+ }
1676
2394
 
1677
2395
  // src/commands.ts
1678
2396
  var USAGE_DAYS = 30;
@@ -1687,6 +2405,7 @@ function parseArgs(argv) {
1687
2405
  global: argv.includes("--global"),
1688
2406
  fix: argv.includes("--fix"),
1689
2407
  allowTracked: argv.includes("--allow-tracked"),
2408
+ json: argv.includes("--json"),
1690
2409
  days: Number.isFinite(days) && days > 0 ? days : USAGE_DAYS,
1691
2410
  dryRun: argv.includes("--dry-run"),
1692
2411
  allowGit: argv.includes("--allow-git"),
@@ -1696,21 +2415,40 @@ function parseArgs(argv) {
1696
2415
  function colorState(state) {
1697
2416
  if (state === "ok") return green(state);
1698
2417
  if (state === "folder missing" || state === "not in library") return red(state);
1699
- if (state === "local only" || state === "from npx skills") return dim(state);
2418
+ if (state === "local only" || state === "from npx skills" || state === "repo skill") return dim(state);
1700
2419
  return yellow(state);
1701
2420
  }
1702
- function printChanges(changes) {
2421
+ function withDirs(root, changes) {
2422
+ const { skills } = readManifest(root);
2423
+ return changes.map((c) => {
2424
+ const dep = c.action === "installed" || c.action === "updated" || c.action === "reset" ? skills[c.name] : void 0;
2425
+ return dep ? { ...c, dirs: [dep.dir ?? ".claude/skills", ...dep.links ?? []] } : c;
2426
+ });
2427
+ }
2428
+ function printChanges(root, changes, args) {
2429
+ if (changes.some((c) => c.action === "skipped")) process.exitCode = 1;
2430
+ if (args.json) return json(withDirs(root, changes));
1703
2431
  for (const c of changes) {
1704
2432
  if (c.blocked?.length) {
1705
2433
  warn(`${c.name}: git tracks ${c.blocked.join(", ")} here, so no link was added there. Re-run with --allow-tracked to add it.`);
1706
2434
  }
1707
- if (c.action === "skipped") warn(`${c.name}: ${c.reason}`);
2435
+ if (c.uncommitted?.length) {
2436
+ warn(
2437
+ `${c.name}: git tracks ${c.uncommitted.join(", ")} here but not ${c.name}'s real copy, so no link was added there (teammates would get a broken link). Commit ${c.name} first, then run skilllib link --allow-tracked.`
2438
+ );
2439
+ }
2440
+ if (c.action === "skipped") error(`${c.name}: ${c.reason}`);
1708
2441
  else {
1709
- const icon = c.action === "removed" ? red("-") : green(c.action === "installed" ? "+" : "\u2191");
2442
+ const icon = c.action === "removed" ? red("-") : c.action === "reset" ? yellow("\u21BA") : green(c.action === "installed" ? "+" : "\u2191");
1710
2443
  const versions = c.action === "removed" ? "" : c.from && c.from !== c.to ? ` v${c.from} \u2192 v${c.to}` : c.to ? ` v${c.to}` : "";
1711
2444
  info(` ${icon} ${c.name}${versions} ${dim(c.action)}`);
1712
2445
  }
2446
+ if (c.backedUp) info(dim(` local edits saved to ${tildify(c.backedUp)}; \`skilllib restore ${c.name}\` brings them back`));
1713
2447
  }
2448
+ const done = changes.filter((c) => c.action !== "skipped").length;
2449
+ const skipped = changes.length - done;
2450
+ if (skipped && done) info(dim(`
2451
+ ${done} done, ${skipped} skipped`));
1714
2452
  }
1715
2453
  function projectOfFactory(projects2) {
1716
2454
  const sorted = [...projects2].sort((a, b) => b.length - a.length);
@@ -1728,40 +2466,98 @@ async function usageBySkill(days, onlyProject) {
1728
2466
  const scoped = onlyProject ? uses.filter((u) => projectOf(u.cwd) === onlyProject) : uses;
1729
2467
  return new Map(summarize(scoped, projectOf).map((s) => [s.skill, s]));
1730
2468
  }
2469
+ function requireProject() {
2470
+ const root = projectHere();
2471
+ if (root) return root;
2472
+ if (!isProjectCandidate(findProjectRoot())) {
2473
+ const stray = existsSync13(join14(userHome(), MANIFEST_FILE)) ? `, though ${tildify(join14(userHome(), MANIFEST_FILE))} makes it look like one` : "";
2474
+ throw new Error(`Your home folder isn't a project${stray}: skills added here would load in every repo. Run this inside a project.`);
2475
+ }
2476
+ throw new Error("Not in a git repo. Run this inside a project (or `git init` first).");
2477
+ }
1731
2478
  function lastUsed(summary) {
1732
2479
  return summary ? summary.lastUsed.slice(0, 10) : "-";
1733
2480
  }
1734
2481
  async function status(args) {
1735
- const root = findProjectRoot();
1736
- const skills = projectStatus(root);
1737
- info(`${basename6(root)} ${dim(tildify(root))}
1738
- `);
1739
- if (skills.length === 0) {
1740
- info("No skills in this project yet. Add one from your library with: skilllib add <name>");
2482
+ if (args.json) {
2483
+ const root2 = findProjectRoot();
2484
+ const usage3 = await usageBySkill(args.days, root2);
2485
+ json(usableHere(root2, new Map([...usage3].map(([name, u]) => [name, u.uses]))));
2486
+ if (projectHere()) rememberProjects([root2]);
1741
2487
  return;
1742
2488
  }
2489
+ const root = requireProject();
2490
+ const skills = projectStatus(root);
2491
+ const nested = nestedSkills(root);
2492
+ info(`${basename8(root)} ${dim(tildify(root))}
2493
+ `);
2494
+ if (skills.length === 0 && nested.length === 0) info("No skills in this project yet. Add one from your library with: skilllib add <name>");
1743
2495
  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
- );
2496
+ const git2 = gitInfo(root, [...new Set(nested.map((s) => s.location))]);
2497
+ const committed = (path) => ["committed", "changed"].includes(git2?.of(relativeTo(root, path)) ?? "");
2498
+ const asRepo = {
2499
+ "local only": "repo skill",
2500
+ "untracked copy of library skill": "repo skill, in library",
2501
+ "untracked, differs from library": "repo skill, differs from library"
2502
+ };
2503
+ const stateOf = (s) => colorState(committed(s.path) && asRepo[s.state] || s.state);
2504
+ const agentsOf = (s) => {
2505
+ const ids = s.visibility.filter((v) => v.paths > 0).map((v) => v.id);
2506
+ return ids.length === s.visibility.length ? dim("all") : ids.length ? ids.map((id) => harness(id).name).join(", ") : red("none");
2507
+ };
2508
+ if (skills.length) {
2509
+ table(
2510
+ skills.map((s) => ({
2511
+ Skill: s.name,
2512
+ Status: s.source ? dim(`npx skills: ${s.source}`) : stateOf(s),
2513
+ Agents: agentsOf(s),
2514
+ [`Uses (${args.days}d)`]: String(usage2.get(s.name)?.uses ?? 0),
2515
+ "Last used": lastUsed(usage2.get(s.name))
2516
+ }))
2517
+ );
2518
+ }
2519
+ if (nested.length) {
2520
+ info(`${skills.length ? "\n" : ""}In subfolders ${dim("(Claude Code loads .claude/skills when you work there; Codex loads .agents/skills when started there)")}`);
2521
+ table(
2522
+ nested.map((s) => ({
2523
+ Skill: s.name,
2524
+ Folder: s.location,
2525
+ Status: stateOf(s),
2526
+ Agents: agentsOf(s),
2527
+ [`Uses (${args.days}d)`]: String(usage2.get(s.name)?.uses ?? 0)
2528
+ }))
2529
+ );
2530
+ }
1752
2531
  const states = new Set(skills.map((s) => s.state));
1753
2532
  const hints = [];
1754
2533
  if (states.has("update available")) hints.push("`skilllib update` to install the newest versions");
1755
- if (states.has("folder missing")) hints.push("`skilllib sync` to restore missing skills");
2534
+ const missing = skills.filter((s) => s.state === "folder missing");
2535
+ if (missing.some((s) => s.latest !== null)) hints.push("`skilllib sync` to restore missing skills");
2536
+ const notInLibrary = missing.filter((s) => s.latest === null).map((s) => s.name);
2537
+ if (notInLibrary.length)
2538
+ hints.push(`${notInLibrary.join(", ")} ${notInLibrary.length === 1 ? "isn't" : "aren't"} in your library (${tildify(libraryDir())}): \`skilllib import <dir>\` to add, or set SKILLLIB_HOME to the library that has ${notInLibrary.length === 1 ? "it" : "them"}`);
1756
2539
  if (states.has("edited locally") || states.has("edited locally, update available"))
1757
2540
  hints.push("`skilllib import .claude/skills/<name> --force` to save local edits to the library");
1758
2541
  if (states.has("local only")) hints.push("`skilllib import .claude/skills/<name>` to add a local skill to the library");
1759
2542
  if (states.has("untracked copy of library skill") || states.has("untracked, differs from library"))
1760
2543
  hints.push("`skilllib add <name>` to manage an untracked skill (--force if it differs)");
1761
2544
  if (hints.length > 0) info("\n" + hints.map((h) => dim(`\u2192 ${h}`)).join("\n"));
2545
+ const here = usableHere(root);
2546
+ const globals = new Set(here.global.flatMap((g) => g.skills)).size;
2547
+ if (globals) info(`
2548
+ ${globals} global ${globals === 1 ? "skill also loads" : "skills also load"} here ${dim("(your global folders, plugins, claude.ai): `skilllib` lists them")}`);
2549
+ const problems = here.issues.reduce((n, i) => n + i.problems.length, 0);
2550
+ if (problems) warn(`${problems} ${problems === 1 ? "issue" : "issues"} here or in your global skills: \`skilllib doctor\` lists them with fixes`);
1762
2551
  rememberProjects([root]);
1763
2552
  }
2553
+ async function overview(args) {
2554
+ if (args.json || projectHere()) return status(args);
2555
+ info("Not in a project. `skilllib projects` lists yours, `skilllib list` your library, `skilllib global` your global skills.");
2556
+ }
1764
2557
  async function list(args) {
2558
+ if (args.json) {
2559
+ return json(libraryFor(projectHere()));
2560
+ }
1765
2561
  const skills = librarySkills();
1766
2562
  if (skills.length === 0) {
1767
2563
  info(`Your library (${tildify(libraryDir())}) is empty.`);
@@ -1770,7 +2566,7 @@ async function list(args) {
1770
2566
  const installs = /* @__PURE__ */ new Map();
1771
2567
  for (const project of visibleProjects()) {
1772
2568
  for (const name of Object.keys(readManifest(project).skills)) {
1773
- installs.set(name, [...installs.get(name) ?? [], basename6(project)]);
2569
+ installs.set(name, [...installs.get(name) ?? [], basename8(project)]);
1774
2570
  }
1775
2571
  }
1776
2572
  const usage2 = await usageBySkill(args.days);
@@ -1788,20 +2584,29 @@ async function list(args) {
1788
2584
  );
1789
2585
  }
1790
2586
  const libraryNames = new Set(skills.map((s) => s.name));
1791
- const globals = machineSkills().filter((m) => m.movable && !m.broken);
2587
+ const globals = [...new Set(machineSkills().filter((m) => m.movable && !m.broken).map((m) => m.name))];
1792
2588
  if (globals.length > 0) {
1793
2589
  info("");
1794
- warn(`${globals.length} of your global skill(s) still load in every project`);
1795
- const missing = globals.filter((g) => !libraryNames.has(g.name));
2590
+ warn(`${globals.length} of your global ${globals.length === 1 ? "skill still loads" : "skills still load"} in every project`);
2591
+ const missing = globals.filter((g) => !libraryNames.has(g));
1796
2592
  info(
1797
2593
  dim(
1798
- missing.length > 0 ? `\u2192 \`skilllib import --global\` copies ${missing.length} of them into the library` : "\u2192 all are in your library; run `skilllib` \u2192 Global skills to stop loading them everywhere"
2594
+ missing.length > 0 ? `\u2192 \`skilllib import --global\` copies ${missing.length} of them into the library` : "\u2192 all are in your library; run `skilllib` \u2192 Global to stop loading them everywhere"
1799
2595
  )
1800
2596
  );
1801
2597
  }
1802
2598
  }
1803
- function projects() {
2599
+ function projects(args) {
1804
2600
  const roots = visibleProjects();
2601
+ if (args.json) {
2602
+ return json(
2603
+ roots.map((root) => ({
2604
+ name: basename8(root),
2605
+ path: root,
2606
+ skills: projectStatus(root).map((s) => ({ name: s.name, managed: s.managed, state: s.state, version: s.version, latest: s.latest }))
2607
+ }))
2608
+ );
2609
+ }
1805
2610
  if (roots.length === 0) {
1806
2611
  info("No known projects. Run `skilllib scan <dir>` to find them, or `skilllib add` inside one.");
1807
2612
  return;
@@ -1812,7 +2617,7 @@ function projects() {
1812
2617
  const managed = skills.filter((s) => s.managed);
1813
2618
  const attention = skills.filter((s) => s.state !== "ok" && s.state !== "local only").length;
1814
2619
  return {
1815
- Project: basename6(root),
2620
+ Project: basename8(root),
1816
2621
  Path: dim(tildify(root)),
1817
2622
  Skills: managed.map((s) => s.name).join(", ") || dim("none"),
1818
2623
  "Local only": String(skills.filter((s) => s.state === "local only").length),
@@ -1826,9 +2631,9 @@ function add(args) {
1826
2631
  error("Usage: skilllib add <skill-name>... [--force]");
1827
2632
  process.exit(1);
1828
2633
  }
1829
- const root = findProjectRoot();
2634
+ const root = requireProject();
1830
2635
  if (args.allowTracked) allowTrackedLinks(root);
1831
- printChanges(args.positional.map((name) => addSkill(root, name, { force: args.force })));
2636
+ printChanges(root, args.positional.map((name) => addSkill(root, name, { force: args.force })), args);
1832
2637
  rememberProjects([root]);
1833
2638
  }
1834
2639
  function remove(args) {
@@ -1836,67 +2641,90 @@ function remove(args) {
1836
2641
  error("Usage: skilllib remove <skill-name>... [--force]");
1837
2642
  process.exit(1);
1838
2643
  }
1839
- const root = findProjectRoot();
1840
- printChanges(args.positional.map((name) => removeSkill(root, name, { force: args.force })));
2644
+ const root = requireProject();
2645
+ printChanges(root, args.positional.map((name) => removeSkill(root, name, { force: args.force })), args);
1841
2646
  }
1842
2647
  function sync(args) {
1843
- const roots = args.all ? visibleProjects() : [findProjectRoot()];
1844
- for (const root of roots) {
2648
+ const roots = args.all ? visibleProjects() : [requireProject()];
2649
+ const results = roots.map((root) => {
1845
2650
  if (args.allowTracked) allowTrackedLinks(root);
1846
2651
  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
- }
2652
+ if (changes.some((c) => c.action === "skipped")) process.exitCode = 1;
2653
+ if (!args.json) {
2654
+ if (args.all) info(`${basename8(root)} ${dim(tildify(root))}`);
2655
+ if (changes.length === 0) {
2656
+ if (existsSync13(join14(root, MANIFEST_FILE))) success("Installed versions match skilllib.json");
2657
+ else info(dim("No skilllib.json: nothing to sync"));
2658
+ } else printChanges(root, changes, args);
2659
+ }
2660
+ return { project: basename8(root), path: root, changes: withDirs(root, changes) };
2661
+ });
2662
+ if (args.json) json(args.all ? results : results[0].changes);
1851
2663
  if (!args.all) rememberProjects(roots);
1852
2664
  }
1853
2665
  function update(args) {
1854
- const roots = args.all ? visibleProjects() : [findProjectRoot()];
1855
- for (const root of roots) {
2666
+ const roots = args.all ? visibleProjects() : [requireProject()];
2667
+ const results = roots.map((root) => {
1856
2668
  if (args.allowTracked) allowTrackedLinks(root);
1857
- 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
- }
2669
+ const names = args.positional.length ? args.positional : void 0;
2670
+ const changes = updateProject(root, names, { force: args.force });
2671
+ if (!args.all) {
2672
+ const { skills } = readManifest(root);
2673
+ for (const name of names ?? []) if (!(name in skills)) changes.push({ name, action: "skipped", reason: "not managed by skilllib in this project" });
2674
+ }
2675
+ if (changes.some((c) => c.action === "skipped")) process.exitCode = 1;
2676
+ if (!args.json) {
2677
+ if (args.all) info(`${basename8(root)} ${dim(tildify(root))}`);
2678
+ if (changes.length === 0) success("Everything is on the latest version");
2679
+ else printChanges(root, changes, args);
2680
+ }
2681
+ return { project: basename8(root), path: root, changes: withDirs(root, changes) };
2682
+ });
2683
+ if (args.json) json(args.all ? results : results[0].changes);
1862
2684
  }
1863
2685
  function outdated(args) {
1864
- 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) }))
2686
+ const roots = args.all ? visibleProjects() : [requireProject()];
2687
+ const behind = roots.flatMap(
2688
+ (root) => projectStatus(root).filter((s) => s.managed && s.latest !== null && s.version !== null && s.latest > s.version).map((s) => ({ project: basename8(root), path: root, skill: s.name, installed: s.version, latest: s.latest, state: s.state }))
1867
2689
  );
2690
+ if (args.json) return json(behind);
2691
+ const rows = behind.map((b) => ({ Project: b.project, Skill: b.skill, Installed: `v${b.installed}`, Latest: `v${b.latest}`, Status: colorState(b.state) }));
1868
2692
  if (rows.length === 0) return success("Everything is on the latest version");
1869
2693
  table(rows);
1870
2694
  info(dim(`
1871
2695
  \u2192 skilllib update${args.all ? " --all" : ""}`));
1872
2696
  }
1873
2697
  function importSkills(args) {
1874
- const dirs = args.global ? machineSkills().filter((m) => m.movable && !m.broken).map((m) => m.path) : args.positional.map((p) => resolve4(p));
2698
+ const dirs = args.global ? machineSkills().filter((m) => m.movable && !m.broken).map((m) => m.path) : args.positional.map((p) => resolve7(p));
1875
2699
  if (dirs.length === 0) {
1876
2700
  error("Usage: skilllib import <skill-dir>... [--force] | skilllib import --global");
1877
2701
  process.exit(1);
1878
2702
  }
2703
+ const results = [];
1879
2704
  for (const dir of dirs) {
1880
2705
  if (!isSkillDir(dir)) {
1881
2706
  warn(`${tildify(dir)}: no SKILL.md, skipping`);
2707
+ results.push({ path: dir, status: "no SKILL.md" });
1882
2708
  continue;
1883
2709
  }
1884
2710
  const { name, status: result } = importSkill(dir, { force: args.force });
2711
+ results.push({ path: dir, name, status: result });
1885
2712
  if (result === "exists") {
1886
2713
  warn(`${name}: the library already has a different version (use --force to replace it)`);
1887
2714
  continue;
1888
2715
  }
1889
2716
  success(`${name} ${dim(result === "unchanged" ? "already in library" : result === "added" ? "added to library" : "library updated")}`);
1890
- const skillsDir = resolve4(dir, "..");
1891
- const root = resolve4(skillsDir, "..", "..");
1892
- if (skillsDir === join13(root, ".claude", "skills") && isProjectCandidate(root)) {
2717
+ const skillsDir = resolve7(dir, "..");
2718
+ const root = resolve7(skillsDir, "..", "..");
2719
+ if (skillsDir === join14(root, ".claude", "skills") && isProjectCandidate(root)) {
1893
2720
  if (readManifest(root).skills[name]?.hash !== treeHash(dir)) {
1894
2721
  const change = addSkill(root, name);
1895
- if (change.action !== "skipped") info(dim(` ${basename6(root)} now depends on ${name} v${change.to}`));
2722
+ if (change.action !== "skipped") info(dim(` ${basename8(root)} now depends on ${name} v${change.to}`));
1896
2723
  }
1897
2724
  rememberProjects([root]);
1898
2725
  }
1899
2726
  }
2727
+ if (args.json) json(results);
1900
2728
  }
1901
2729
  function scan(args) {
1902
2730
  for (const p of args.positional) addRoot(p);
@@ -1907,24 +2735,34 @@ function scan(args) {
1907
2735
  }
1908
2736
  const found = discoverProjects();
1909
2737
  const library = new Set(librarySkills().map((s) => s.name));
1910
- const skillNames = (root) => [...skillDirsIn(projectSkillsDir(root)), ...skillDirsIn(agentsSkillsDir(root))].map((d) => basename6(d));
2738
+ const skillNames = (root) => [
2739
+ .../* @__PURE__ */ new Set([...ALL_PROJECT_DIRS.flatMap((d) => skillDirsIn(join14(root, d))).map((d) => basename8(d)), ...nestedSkills(root).map((s) => s.name)])
2740
+ ];
1911
2741
  const withSkills = found.filter((root) => skillNames(root).length > 0);
1912
- info(`Scanned ${roots.map((r) => tildify(r)).join(", ")}: ${found.length} project(s), ${withSkills.length} with skills
2742
+ info(`Scanned ${roots.map((r) => tildify(r)).join(", ")}: ${found.length} project${found.length === 1 ? "" : "s"}, ${withSkills.length} with skills
1913
2743
  `);
1914
2744
  table(
1915
2745
  withSkills.map((root) => ({
1916
- Project: basename6(root),
2746
+ Project: basename8(root),
1917
2747
  Path: dim(tildify(root)),
1918
- Skills: [...new Set(skillNames(root))].map((n) => library.has(n) ? n : yellow(n)).join(", ")
2748
+ Skills: skillNames(root).map((n) => library.has(n) ? n : yellow(n)).join(", ")
1919
2749
  }))
1920
2750
  );
1921
2751
  if (withSkills.length > 0) info(`
1922
- ${yellow("Yellow")} skills aren't in your library yet (import them from \`skilllib\` \u2192 3 Sources or 5 Health).`);
2752
+ ${yellow("Yellow")} skills aren't in your library yet (\`skilllib import <skill-dir>\`, or \`skilllib\` \u2192 Health).`);
1923
2753
  }
1924
2754
  function folders(args) {
1925
2755
  const [action, ...paths] = args.positional;
2756
+ let failed = false;
1926
2757
  if (action === "add") {
1927
- for (const p of paths) success(`Added ${tildify(addRoot(p))}`);
2758
+ for (const p of paths) {
2759
+ try {
2760
+ success(`Added ${tildify(addRoot(p))}`);
2761
+ } catch (e) {
2762
+ error(e.message);
2763
+ failed = true;
2764
+ }
2765
+ }
1928
2766
  discoverProjects();
1929
2767
  } else if (action === "remove" || action === "rm") {
1930
2768
  for (const p of paths) {
@@ -1936,12 +2774,24 @@ function folders(args) {
1936
2774
  info(roots.length ? `Project folders:
1937
2775
  ${roots.map((r) => ` ${tildify(r)}`).join("\n")}` : "No project folders. Add one: skilllib folders add ~/Projects");
1938
2776
  if (hidden.length) info(dim(`Hidden projects: ${hidden.map((h) => tildify(h)).join(", ")} (skilllib unhide <path>)`));
2777
+ if (failed) process.exit(1);
1939
2778
  }
1940
2779
  function unhide(args) {
2780
+ if (args.positional.length === 0) {
2781
+ error("Usage: skilllib unhide <path>...");
2782
+ process.exit(1);
2783
+ }
2784
+ let failed = false;
1941
2785
  for (const p of args.positional) {
1942
- setHidden(expandHome(p), false);
1943
- success(`${tildify(expandHome(p))} is visible again`);
2786
+ const path = expandHome(p);
2787
+ if (setHidden(path, false)) success(`${tildify(path)} is visible again`);
2788
+ else {
2789
+ const { hidden } = readConfig();
2790
+ error(`${tildify(path)} wasn't hidden. ${hidden.length ? `Hidden projects: ${hidden.map((h) => tildify(h)).join(", ")}` : "No projects are hidden."}`);
2791
+ failed = true;
2792
+ }
1944
2793
  }
2794
+ if (failed) process.exit(1);
1945
2795
  }
1946
2796
  function newSkill(args) {
1947
2797
  const [name, ...rest] = args.positional;
@@ -1954,14 +2804,14 @@ function newSkill(args) {
1954
2804
  error(`Can't create ${name}: ${r.reason}`);
1955
2805
  process.exit(1);
1956
2806
  }
1957
- success(`Created ${tildify(join13(r.dir, "SKILL.md"))}`);
2807
+ success(`Created ${tildify(join14(r.dir, "SKILL.md"))}`);
1958
2808
  }
1959
2809
  function restore(args) {
1960
2810
  const backups = [...listBackups(), ...pluginBackups()];
1961
2811
  const name = args.positional[0];
1962
2812
  if (!name) {
1963
2813
  if (backups.length === 0) return info("Nothing to restore.");
1964
- table(backups.map((b) => ({ Skill: b.name, From: b.kind === "trash" ? "library (deleted)" : b.kind === "plugin" ? "Claude Code plugin" : tildify(dirname5(b.from)), Moved: b.movedAt })));
2814
+ table(backups.map((b) => ({ Skill: b.name, From: backupFrom(b), Moved: b.movedAt })));
1965
2815
  info(dim("\n\u2192 skilllib restore <name>"));
1966
2816
  return;
1967
2817
  }
@@ -1972,18 +2822,51 @@ function restore(args) {
1972
2822
  }
1973
2823
  const r = backup.kind === "plugin" ? restorePlugin(backup) : restoreBackup(backup);
1974
2824
  if (r.ok) success(`Restored ${name} to ${tildify(r.to)}`);
1975
- else error(`Can't restore ${name}: ${r.reason}`);
2825
+ else {
2826
+ error(`Can't restore ${name}: ${r.reason}`);
2827
+ process.exitCode = 1;
2828
+ }
1976
2829
  }
1977
- function doctor(args) {
2830
+ async function doctor(args) {
1978
2831
  const projects2 = visibleProjects();
1979
- const issues = findIssues(projects2, machineSkills(), new Set(librarySkills().map((s) => s.name)));
2832
+ const machine = machineSkills();
2833
+ const libraryNames = new Set(librarySkills().map((s) => s.name));
2834
+ const issues = findIssues(projects2, machine, libraryNames);
2835
+ if (enabledHarnesses().includes("claude-code")) {
2836
+ const projectOf = projectOfFactory(projects2);
2837
+ const uses = await scanUsage(args.days);
2838
+ const inProject = usesByProject(uses, projectOf);
2839
+ const total = new Map(summarize(uses, projectOf).map((s) => [s.skill, s.uses]));
2840
+ issues.push(
2841
+ ...usageIssues(projects2, machine, libraryNames, {
2842
+ inProject: (root, skill) => inProject.get(root)?.get(skill) ?? 0,
2843
+ total: (skill) => total.get(skill) ?? 0,
2844
+ days: args.days
2845
+ })
2846
+ );
2847
+ }
2848
+ const fixed = (fix) => {
2849
+ const r = runFix(fix);
2850
+ if (!r.ok) process.exitCode = 1;
2851
+ return r;
2852
+ };
2853
+ if (args.json) {
2854
+ return json(
2855
+ issues.map(({ fix, choices, ...issue }) => ({
2856
+ ...issue,
2857
+ ...fix && { fix: fix.label },
2858
+ ...choices && { choices: choices.map((c) => c.label) },
2859
+ ...args.fix && fix && { fixed: fixed(fix) }
2860
+ }))
2861
+ );
2862
+ }
1980
2863
  if (issues.length === 0) return success("Everything looks good");
1981
2864
  for (const issue of issues) {
1982
2865
  info(`${issue.severity === "problem" ? red("\u25CF") : yellow("\u25C6")} ${issue.title}`);
1983
2866
  info(dim(` ${issue.detail}${issue.fix ? ` \u2192 ${issue.fix.label}` : ""}`));
1984
2867
  if (issue.choices) info(dim(` choose in skilllib \u2192 Health: ${issue.choices.map((c) => c.label).join(" / ")}`));
1985
2868
  if (args.fix && issue.fix) {
1986
- const r = runFix(issue.fix);
2869
+ const r = fixed(issue.fix);
1987
2870
  info(` ${r.ok ? green("\u2713") : red("\u2717")} ${r.message}`);
1988
2871
  }
1989
2872
  }
@@ -1991,18 +2874,27 @@ function doctor(args) {
1991
2874
  }
1992
2875
  async function usage(args) {
1993
2876
  const summaries = summarize(await scanUsage(args.days), projectOfFactory(knownProjects()));
2877
+ if (args.json) {
2878
+ const library2 = new Set(librarySkills().map((s) => s.name));
2879
+ return json({
2880
+ usageDays: args.days,
2881
+ source: "Claude Code transcripts",
2882
+ 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) })),
2883
+ unusedLibrarySkills: librarySkills().filter((l) => !summaries.some((u) => u.skill === l.name)).map((l) => l.name)
2884
+ });
2885
+ }
1994
2886
  if (summaries.length === 0) {
1995
2887
  info(`No skill usage in Claude Code transcripts from the last ${args.days} days.`);
1996
2888
  return;
1997
2889
  }
1998
2890
  const library = new Set(librarySkills().map((s) => s.name));
1999
- info(`Skill usage, last ${args.days} days ${dim(`(from ${tildify(join13(claudeDir(), "projects"))})`)}
2891
+ info(`Skill usage, last ${args.days} days ${dim(`(from ${tildify(join14(claudeDir(), "projects"))})`)}
2000
2892
  `);
2001
2893
  table(
2002
2894
  summaries.map((s) => ({
2003
2895
  Skill: s.skill,
2004
2896
  Uses: String(s.uses),
2005
- Projects: [...s.projects].map((p) => basename6(p)).join(", ") || dim("-"),
2897
+ Projects: [...s.projects].map((p) => basename8(p)).join(", ") || dim("-"),
2006
2898
  "Last used": lastUsed(s),
2007
2899
  Library: library.has(s.skill) ? green("yes") : dim("no")
2008
2900
  }))
@@ -2015,12 +2907,23 @@ async function usage(args) {
2015
2907
  }
2016
2908
  function show(args) {
2017
2909
  const name = args.positional[0];
2018
- const dir = name ? join13(libraryDir(), name) : "";
2910
+ const dir = name ? join14(libraryDir(), name) : "";
2019
2911
  if (!name || !isSkillDir(dir)) {
2020
2912
  error(name ? `${name} is not in the library` : "Usage: skilllib show <skill-name>");
2021
2913
  process.exit(1);
2022
2914
  }
2023
2915
  const { description } = readSkillInfo(dir);
2916
+ if (args.json) {
2917
+ return json({
2918
+ name,
2919
+ description,
2920
+ path: dir,
2921
+ latest: latestVersion(name)?.version ?? null,
2922
+ origin: libraryOrigins()[name] ?? "",
2923
+ projects: knownProjects().filter((p) => name in readManifest(p).skills).map((p) => basename8(p)),
2924
+ skillMd: readFileSync9(join14(dir, "SKILL.md"), "utf-8")
2925
+ });
2926
+ }
2024
2927
  info(`${name} ${dim(tildify(dir))}`);
2025
2928
  if (description) info(`
2026
2929
  ${description}`);
@@ -2047,14 +2950,21 @@ function harnessesCommand(args) {
2047
2950
  New installs go to ${installDirs(enabled).join(" (real copy) + ")}${installDirs(enabled).length > 1 ? " (link)" : ""}.`));
2048
2951
  }
2049
2952
  function link(args) {
2050
- const roots = args.all ? visibleProjects() : [findProjectRoot()];
2953
+ const roots = args.all ? visibleProjects() : [requireProject()];
2051
2954
  for (const root of roots) {
2052
2955
  if (args.allowTracked) allowTrackedLinks(root);
2053
2956
  const res = linkAll(root);
2054
- const label = args.all ? `${basename6(root)}: ` : "";
2957
+ const label = args.all ? `${basename8(root)}: ` : "";
2055
2958
  if (res.linked.length) success(`${label}linked ${res.linked.length} skill(s) for your other agents`);
2056
- else if (!args.all) success("Every skill here is already usable by all your agents");
2057
- if (res.blocked.length) warn(`${label}git tracks ${res.blocked.join(", ")}; re-run with --allow-tracked to link there too`);
2959
+ else if (!args.all && !res.blocked.length && !res.uncommitted.length) success("Every skill here is already usable by all your agents");
2960
+ if (res.blocked.length) {
2961
+ process.exitCode = 1;
2962
+ error(`${label}git tracks ${res.blocked.join(", ")}, so ${res.linked.length ? "some skills weren't" : "nothing was"} linked there; re-run with --allow-tracked to link there too`);
2963
+ }
2964
+ if (res.uncommitted.length)
2965
+ warn(
2966
+ `${label}${res.uncommitted.join(", ")} ${res.uncommitted.length === 1 ? "isn't" : "aren't"} committed, so not linked into folders git tracks (teammates would get broken links); commit ${res.uncommitted.length === 1 ? "it" : "them"} first`
2967
+ );
2058
2968
  }
2059
2969
  }
2060
2970
  function globalCommand(args) {
@@ -2075,14 +2985,41 @@ function globalCommand(args) {
2075
2985
  const kept = keptGlobal();
2076
2986
  if (!yours.length) return info("None of your skills load globally.");
2077
2987
  for (const m of yours) info(`${kept.has(m.name) ? green("\u2713") : yellow("\u26A0")} ${m.name.padEnd(32)} ${dim(tildify(m.path))}`);
2078
- const unreviewed = yours.filter((m) => !kept.has(m.name)).length;
2988
+ const unreviewed = new Set(yours.filter((m) => !kept.has(m.name)).map((m) => m.name)).size;
2079
2989
  if (unreviewed) info(dim(`
2080
2990
  ${unreviewed} not reviewed. Keep one global on purpose: skilllib global keep <name>`));
2081
2991
  }
2082
2992
  function tidy(args) {
2083
2993
  const names = args.positional;
2084
- 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) }) }));
2994
+ const keepFor = (base) => args.keep ? Object.fromEntries(names.map((n) => [n, base ? resolve7(base, args.keep) : expandHome(args.keep)])) : {};
2995
+ if (args.keep && !names.length) {
2996
+ error("Usage: skilllib tidy <name>... --keep <folder>");
2997
+ process.exit(1);
2998
+ }
2999
+ const targets = args.global ? [null] : args.all ? visibleProjects() : [requireProject()];
3000
+ const problems = [];
3001
+ for (const name of names) {
3002
+ const where = targets.map((root) => ({ root, ...foldersOf(root, name) })).filter((w) => w.all.length);
3003
+ if (!where.length) {
3004
+ problems.push(`${name}: no skill by that name ${args.global ? "in your global folders" : args.all ? "in your projects" : `in ${basename8(targets[0])}`}`);
3005
+ continue;
3006
+ }
3007
+ if (!args.keep) continue;
3008
+ for (const { root, real } of where) {
3009
+ if (real.includes(keepFor(root)[name])) continue;
3010
+ const label = root ? `${basename8(root)} \xB7 ` : "";
3011
+ problems.push(
3012
+ `${label}${name}: no copy in ${args.keep}; ${real.length ? `pick one of: ${real.map((d) => folderLabel(root, d)).join(", ")}` : "it has no real copy to keep"}`
3013
+ );
3014
+ }
3015
+ }
3016
+ if (problems.length) {
3017
+ for (const p of problems) error(p);
3018
+ process.exit(1);
3019
+ }
3020
+ const reports = targets.map(
3021
+ (root) => root === null ? { label: "global folders", report: planGlobalTidy({ keep: keepFor(null) }) } : { label: basename8(root), report: planProjectTidy(root, { keep: keepFor(root) }) }
3022
+ );
2086
3023
  let held = 0;
2087
3024
  let changed = 0;
2088
3025
  for (const { label, report } of reports) {
@@ -2097,6 +3034,7 @@ function tidy(args) {
2097
3034
  held += args.dryRun ? 0 : result.held.length;
2098
3035
  changed += result.applied.length;
2099
3036
  }
3037
+ if (args.keep && conflicts.length) process.exitCode = 1;
2100
3038
  for (const c of conflicts) {
2101
3039
  warn(`${label} \xB7 ${c.name}: the copies differ`);
2102
3040
  for (const copy of c.copies) {
@@ -2112,18 +3050,46 @@ function tidy(args) {
2112
3050
  else if (changed) info(dim("\nReplaced copies are in ~/.skilllib/tidy-backup; `skilllib restore` puts them back."));
2113
3051
  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
3052
  }
3053
+ function agentSkill(args) {
3054
+ const action = args.positional[0];
3055
+ if (action === "install") {
3056
+ const r = installAgentSkill();
3057
+ if (!r.ok) {
3058
+ error(`Can't install the skilllib skill: ${r.reason}`);
3059
+ process.exit(1);
3060
+ }
3061
+ if (args.json) return json({ state: agentSkillState(), dirs: r.dirs });
3062
+ success(`Installed in ${r.dirs.map((d) => tildify(d)).join(" and ")}`);
3063
+ if (!onPath("skilllib")) warn("`skilllib` isn't on your PATH, so agents can't run it. Install it: npm install -g skilllib");
3064
+ info(dim("New agent sessions can now answer questions like \u201Cwhich skills can you use here?\u201D"));
3065
+ return;
3066
+ }
3067
+ if (action === "remove" || action === "rm") {
3068
+ const removed = removeAgentSkill();
3069
+ if (args.json) return json({ state: agentSkillState(), removed });
3070
+ return removed.length ? success(`Removed from ${removed.map((d) => tildify(d)).join(" and ")}`) : info("The skilllib skill isn't installed.");
3071
+ }
3072
+ const state = agentSkillState();
3073
+ if (args.json) return json({ state, dirs: agentSkillDirs() });
3074
+ if (state === "installed") success(`Your agents can use skilllib ${dim(`(${agentSkillDirs().map((d) => tildify(d)).join(", ")})`)}`);
3075
+ else if (state === "outdated") warn("The skilllib skill is out of date. Run `skilllib agent-skill install` to update it.");
3076
+ else info("Your agents don't know about skilllib yet. Run `skilllib agent-skill install` so you can ask them about your skills.");
3077
+ }
2115
3078
 
2116
3079
  export {
2117
- userHome,
2118
3080
  skilllibHome,
3081
+ PROJECT_SKILLS_DIR,
2119
3082
  HARNESSES,
2120
3083
  harness,
2121
3084
  readSkillInfo,
2122
3085
  versionHistory,
2123
3086
  forgetLatest,
2124
3087
  latestVersion,
2125
- findProjectRoot,
2126
- isProjectCandidate,
3088
+ projectHere,
3089
+ readManifest,
3090
+ setJsonMode,
3091
+ error,
3092
+ tildify,
2127
3093
  readConfig,
2128
3094
  addRoot,
2129
3095
  removeRoot,
@@ -2138,10 +3104,12 @@ export {
2138
3104
  setKeepGlobal,
2139
3105
  setPluginEnabled,
2140
3106
  machineSkills,
3107
+ skillsLoadedIn,
2141
3108
  libraryOrigins,
2142
3109
  recordOrigin,
2143
3110
  librarySkills,
2144
3111
  librarySkillDir,
3112
+ visibilityOf,
2145
3113
  projectStatus,
2146
3114
  linkEverywhere,
2147
3115
  relinkDependency,
@@ -2155,20 +3123,35 @@ export {
2155
3123
  deleteLibrarySkill,
2156
3124
  deleteGlobal,
2157
3125
  unloadGlobal,
3126
+ linkGlobal,
2158
3127
  createSkill,
2159
3128
  listBackups,
2160
3129
  restoreBackup,
2161
- error,
2162
- scanUsage,
2163
3130
  gitInfo,
2164
3131
  relativeTo,
3132
+ claudePlugins,
2165
3133
  claudeBinary,
3134
+ turnOffIn,
2166
3135
  recordRemovedPlugin,
2167
3136
  pluginBackups,
2168
3137
  restorePlugin,
3138
+ cursorPluginSkills,
3139
+ agentSkillState,
3140
+ installAgentSkill,
3141
+ removeAgentSkill,
3142
+ refreshAgentSkill,
3143
+ planProjectTidy,
3144
+ planGlobalTidy,
3145
+ gitVisibleSteps,
3146
+ applyTidy,
3147
+ describeStep,
3148
+ copyLabel,
3149
+ addedAt,
3150
+ scanUsage,
2169
3151
  parseArgs,
2170
3152
  projectOfFactory,
2171
3153
  status,
3154
+ overview,
2172
3155
  list,
2173
3156
  projects,
2174
3157
  add,
@@ -2188,5 +3171,6 @@ export {
2188
3171
  harnessesCommand,
2189
3172
  link,
2190
3173
  globalCommand,
2191
- tidy
3174
+ tidy,
3175
+ agentSkill
2192
3176
  };