skilllib 1.6.0 → 1.10.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,11 +1,53 @@
1
1
  // src/output.ts
2
- import { sep } from "path";
2
+ import { resolve as resolve2, sep } from "path";
3
+
4
+ // src/paths.ts
5
+ import { realpathSync } from "fs";
3
6
  import { homedir } from "os";
7
+ import { join, resolve } from "path";
8
+ function userHome() {
9
+ return process.env.HOME || homedir();
10
+ }
11
+ function realPath(path) {
12
+ try {
13
+ return realpathSync(resolve(path));
14
+ } catch {
15
+ return resolve(path);
16
+ }
17
+ }
18
+ function skilllibHome() {
19
+ return process.env.SKILLLIB_HOME ?? join(userHome(), ".skilllib");
20
+ }
21
+ function libraryDir() {
22
+ return join(skilllibHome(), "library");
23
+ }
24
+ function claudeDir() {
25
+ return process.env.CLAUDE_CONFIG_DIR ?? join(userHome(), ".claude");
26
+ }
27
+ function grokDir() {
28
+ return process.env.GROK_HOME?.trim() || join(userHome(), ".grok");
29
+ }
30
+ function codexHome() {
31
+ return process.env.CODEX_HOME?.trim() || join(userHome(), ".codex");
32
+ }
33
+ function codexSystemDir() {
34
+ return process.env.SKILLLIB_CODEX_SYSTEM_DIR ?? "/etc/codex/skills";
35
+ }
36
+ var PROJECT_SKILLS_DIR = ".claude/skills";
37
+ var AGENTS_SKILLS_DIR = ".agents/skills";
38
+ var GROK_SKILLS_DIR = ".grok/skills";
39
+ var MANIFEST_FILE = "skilllib.json";
40
+ function isRepoSkillLocation(location) {
41
+ return location === AGENTS_SKILLS_DIR || location === GROK_SKILLS_DIR;
42
+ }
43
+ var OWN_SKILL_MARKER = ".managed-by-skilllib";
44
+
45
+ // src/output.ts
4
46
  var jsonMode = false;
5
47
  function setJsonMode(on) {
6
48
  jsonMode = on;
7
49
  }
8
- function json(data) {
50
+ function json(output, data) {
9
51
  process.stdout.write(JSON.stringify(data) + "\n");
10
52
  }
11
53
  var color = (code) => (s) => process.stdout.isTTY && !jsonMode ? `\x1B[${code}m${s}\x1B[0m` : s;
@@ -19,8 +61,9 @@ var success = (msg) => out(`${green("\u2713")} ${msg}`);
19
61
  var warn = (msg) => out(`${yellow("!")} ${msg}`);
20
62
  var error = (msg) => console.error(`${red("\u2717")} ${msg}`);
21
63
  function tildify(path) {
22
- const home2 = homedir();
23
- return path === home2 || path.startsWith(home2 + sep) ? "~" + path.slice(home2.length) : path;
64
+ for (const home2 of /* @__PURE__ */ new Set([resolve2(userHome()), realPath(userHome())]))
65
+ if (path === home2 || path.startsWith(home2 + sep)) return ("~" + path.slice(home2.length)).replace(/\\/g, "/");
66
+ return path;
24
67
  }
25
68
  function truncate(s, max) {
26
69
  const line = s.replace(/\s+/g, " ");
@@ -41,35 +84,9 @@ import { existsSync as existsSync8, lstatSync as lstatSync3, mkdirSync as mkdirS
41
84
  import { join as join9 } from "path";
42
85
 
43
86
  // src/config.ts
44
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
45
- import { homedir as homedir4 } from "os";
46
- import { join as join6, resolve as resolve2 } from "path";
47
-
48
- // src/paths.ts
87
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
49
88
  import { homedir as homedir2 } from "os";
50
- import { join } from "path";
51
- function userHome() {
52
- return process.env.HOME || homedir2();
53
- }
54
- function skilllibHome() {
55
- return process.env.SKILLLIB_HOME ?? join(userHome(), ".skilllib");
56
- }
57
- function libraryDir() {
58
- return join(skilllibHome(), "library");
59
- }
60
- function claudeDir() {
61
- return process.env.CLAUDE_CONFIG_DIR ?? join(userHome(), ".claude");
62
- }
63
- function codexHome() {
64
- return process.env.CODEX_HOME?.trim() || join(userHome(), ".codex");
65
- }
66
- function codexSystemDir() {
67
- return process.env.SKILLLIB_CODEX_SYSTEM_DIR ?? "/etc/codex/skills";
68
- }
69
- var PROJECT_SKILLS_DIR = ".claude/skills";
70
- var AGENTS_SKILLS_DIR = ".agents/skills";
71
- var MANIFEST_FILE = "skilllib.json";
72
- var OWN_SKILL_MARKER = ".managed-by-skilllib";
89
+ import { join as join6, resolve as resolve4 } from "path";
73
90
 
74
91
  // src/harnesses.ts
75
92
  import { existsSync } from "fs";
@@ -93,7 +110,7 @@ var HARNESSES = [
93
110
  icon: "\u2B21",
94
111
  color: "#E8E8E8",
95
112
  // The first folder is where links for Cursor go (see linkEverywhere). Cursor also reads .grok/skills;
96
- // skilllib leaves that folder to Grok, since it can't tell what Grok needs.
113
+ // skilllib leaves that folder alone (see Grok). A .grok-only skill stays invisible to Cursor here.
97
114
  projectDirs: [".agents/skills", ".cursor/skills", ".claude/skills", ".codex/skills"],
98
115
  // Cursor ignores CLAUDE_CONFIG_DIR and CODEX_HOME: it always reads these under your home folder.
99
116
  globalDirs: () => [".agents", ".cursor", ".claude", ".codex"].map((d) => join2(home(), d, "skills")),
@@ -118,6 +135,19 @@ var HARNESSES = [
118
135
  projectDirs: [".agents/skills"],
119
136
  globalDirs: () => [join2(home(), ".agents", "skills")],
120
137
  installed: () => existsSync(join2(home(), ".config", "zed")) || onPath("zed") || hasApp("Zed")
138
+ },
139
+ {
140
+ id: "grok",
141
+ name: "Grok",
142
+ short: "Gk",
143
+ icon: "\u2736",
144
+ color: "#F5C16C",
145
+ // .agents/skills is first so a skill Grok can't already see is linked into the shared folder, not .grok/skills.
146
+ projectDirs: [".agents/skills", ".grok/skills", ".claude/skills", ".cursor/skills"],
147
+ // Grok ignores CLAUDE_CONFIG_DIR: the Claude compat path is always ~/.claude/skills.
148
+ // The first folder is where global links for Grok go, so those links stay out of ~/.grok/skills.
149
+ globalDirs: () => [join2(home(), ".agents", "skills"), join2(grokDir(), "skills"), join2(home(), ".claude", "skills"), join2(home(), ".cursor", "skills")],
150
+ installed: () => existsSync(grokDir()) || onPath("grok") || hasApp("Grok")
121
151
  }
122
152
  ];
123
153
  function onPath(bin) {
@@ -155,8 +185,7 @@ function installDirs(enabled, keep) {
155
185
 
156
186
  // src/project.ts
157
187
  import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
158
- import { homedir as homedir3 } from "os";
159
- import { dirname, join as join5, resolve } from "path";
188
+ import { dirname, join as join5, resolve as resolve3 } from "path";
160
189
 
161
190
  // src/versions.ts
162
191
  import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
@@ -164,7 +193,7 @@ import { join as join4 } from "path";
164
193
 
165
194
  // src/skills.ts
166
195
  import { createHash } from "crypto";
167
- import { cpSync, existsSync as existsSync2, readdirSync, readFileSync, realpathSync, rmSync, statSync } from "fs";
196
+ import { cpSync, existsSync as existsSync2, readdirSync, readFileSync, realpathSync as realpathSync2, rmSync, statSync } from "fs";
168
197
  import { basename, join as join3, relative, sep as sep2 } from "path";
169
198
  import { parse as parseYaml } from "yaml";
170
199
  var IGNORED = /* @__PURE__ */ new Set([".DS_Store", ".git", "node_modules"]);
@@ -204,7 +233,7 @@ function treeHash(dir) {
204
233
  }
205
234
  function copySkill(from, to) {
206
235
  rmSync(to, { recursive: true, force: true });
207
- cpSync(realpathSync(from), to, { recursive: true, dereference: true, filter: (src) => !IGNORED.has(basename(src)) });
236
+ cpSync(realpathSync2(from), to, { recursive: true, dereference: true, filter: (src) => !IGNORED.has(basename(src)) });
208
237
  }
209
238
  function skillDirsIn(dir) {
210
239
  if (!existsSync2(dir)) return [];
@@ -263,19 +292,17 @@ function getVersion(name, version) {
263
292
 
264
293
  // src/project.ts
265
294
  function findProjectRoot(from = process.cwd()) {
266
- let dir = resolve(from);
295
+ let dir = resolve3(from);
267
296
  while (true) {
268
297
  if (existsSync4(join5(dir, MANIFEST_FILE)) || existsSync4(join5(dir, ".git"))) return dir;
269
298
  const parent = dirname(dir);
270
- if (parent === dir) return resolve(from);
299
+ if (parent === dir) return resolve3(from);
271
300
  dir = parent;
272
301
  }
273
302
  }
274
- function projectSkillsDir(root) {
275
- return join5(root, PROJECT_SKILLS_DIR);
276
- }
277
- function agentsSkillsDir(root) {
278
- return join5(root, AGENTS_SKILLS_DIR);
303
+ function projectHere(from = process.cwd()) {
304
+ const root = findProjectRoot(from);
305
+ return isProjectCandidate(root) && (existsSync4(join5(root, ".git")) || existsSync4(join5(root, MANIFEST_FILE))) ? root : null;
279
306
  }
280
307
  function readManifest(root) {
281
308
  const path = join5(root, MANIFEST_FILE);
@@ -315,14 +342,23 @@ function knownProjects() {
315
342
  const path = projectsFile();
316
343
  if (!existsSync4(path)) return [];
317
344
  const list2 = JSON.parse(readFileSync3(path, "utf-8"));
318
- return list2.filter((p) => existsSync4(p) && isProjectCandidate(p));
345
+ return onePerFolder(list2.filter((p) => existsSync4(p) && isProjectCandidate(p)));
346
+ }
347
+ function onePerFolder(paths) {
348
+ const byReal = /* @__PURE__ */ new Map();
349
+ for (const p of paths) {
350
+ const real = realPath(p);
351
+ const seen = byReal.get(real);
352
+ if (seen === void 0 || seen === real && p !== real) byReal.set(real, p);
353
+ }
354
+ return [...new Set(byReal.values())];
319
355
  }
320
356
  function isProjectCandidate(root) {
321
- return resolve(root) !== homedir3();
357
+ return realPath(root) !== realPath(userHome());
322
358
  }
323
359
  function rememberProjects(roots) {
324
- const incoming = roots.map((r) => resolve(r)).filter(isProjectCandidate);
325
- const all = [.../* @__PURE__ */ new Set([...knownProjects(), ...incoming])].sort();
360
+ const incoming = roots.map((r) => resolve3(r)).filter(isProjectCandidate);
361
+ const all = onePerFolder([...knownProjects(), ...incoming]).sort();
326
362
  mkdirSync2(skilllibHome(), { recursive: true });
327
363
  writeFileSync2(projectsFile(), JSON.stringify(all, null, 2) + "\n");
328
364
  }
@@ -344,10 +380,12 @@ function writeConfig(config) {
344
380
  writeFileSync3(configFile(), JSON.stringify(config, null, 2) + "\n");
345
381
  }
346
382
  function expandHome(path) {
347
- return resolve2(path.replace(/^~(?=$|\/)/, homedir4()));
383
+ return resolve4(path.replace(/^~(?=$|\/)/, homedir2()));
348
384
  }
349
385
  function addRoot(path) {
350
386
  const root = expandHome(path);
387
+ if (!existsSync5(root)) throw new Error(`${tildify(root)} doesn't exist`);
388
+ if (!statSync2(root).isDirectory()) throw new Error(`${tildify(root)} isn't a folder`);
351
389
  const config = readConfig();
352
390
  if (!config.roots.includes(root)) writeConfig({ ...config, roots: [...config.roots, root].sort() });
353
391
  return root;
@@ -361,8 +399,10 @@ function removeRoot(path) {
361
399
  }
362
400
  function setHidden(project, hidden) {
363
401
  const config = readConfig();
402
+ if (config.hidden.includes(project) === hidden) return false;
364
403
  const rest = config.hidden.filter((h) => h !== project);
365
404
  writeConfig({ ...config, hidden: hidden ? [...rest, project].sort() : rest });
405
+ return true;
366
406
  }
367
407
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "vendor", "target", "Library", "Applications", "Pictures", "Music", "Movies"]);
368
408
  function findProjects(dir, depth = 4) {
@@ -386,8 +426,8 @@ function discoverProjects() {
386
426
  return visibleProjects();
387
427
  }
388
428
  function visibleProjects() {
389
- const hidden = new Set(readConfig().hidden);
390
- return knownProjects().filter((p) => !hidden.has(p));
429
+ const hidden = new Set(readConfig().hidden.map(realPath));
430
+ return knownProjects().filter((p) => !hidden.has(realPath(p)));
391
431
  }
392
432
  function enabledHarnesses() {
393
433
  return readConfig().harnesses ?? HARNESSES.filter((h) => h.installed()).map((h) => h.id);
@@ -418,12 +458,12 @@ function setKeepGlobal(names, keep) {
418
458
 
419
459
  // src/library.ts
420
460
  import { execFileSync } from "child_process";
421
- import { existsSync as existsSync7, lstatSync as lstatSync2, mkdirSync as mkdirSync5, readdirSync as readdirSync4, readFileSync as readFileSync6, realpathSync as realpathSync3, renameSync, rmSync as rmSync2, symlinkSync, unlinkSync, writeFileSync as writeFileSync5 } from "fs";
461
+ import { existsSync as existsSync7, lstatSync as lstatSync2, mkdirSync as mkdirSync5, readdirSync as readdirSync4, readFileSync as readFileSync6, realpathSync as realpathSync4, renameSync, rmSync as rmSync2, symlinkSync, unlinkSync, writeFileSync as writeFileSync5 } from "fs";
422
462
  import { basename as basename3, dirname as dirname2, join as join8, relative as relative2, sep as sep3 } from "path";
423
463
 
424
464
  // src/sources.ts
425
- import { existsSync as existsSync6, lstatSync, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync5, readlinkSync, realpathSync as realpathSync2, writeFileSync as writeFileSync4 } from "fs";
426
- import { basename as basename2, join as join7, resolve as resolve3 } from "path";
465
+ import { existsSync as existsSync6, lstatSync, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync5, readlinkSync, realpathSync as realpathSync3, writeFileSync as writeFileSync4 } from "fs";
466
+ import { basename as basename2, join as join7, resolve as resolve5 } from "path";
427
467
  function readJson(path) {
428
468
  try {
429
469
  return JSON.parse(readFileSync5(path, "utf-8"));
@@ -432,24 +472,36 @@ function readJson(path) {
432
472
  }
433
473
  }
434
474
  function skillsShLock() {
435
- return readJson(join7(resolve3(claudeDir(), ".."), ".agents", ".skill-lock.json"))?.skills ?? {};
475
+ return readJson(join7(userHome(), ".agents", ".skill-lock.json"))?.skills ?? {};
436
476
  }
437
477
  function projectSkillsLock(root) {
438
478
  return readJson(join7(root, "skills-lock.json"))?.skills ?? {};
439
479
  }
440
480
  function realpathOrNull(path) {
441
481
  try {
442
- return realpathSync2(path);
482
+ return realpathSync3(path);
443
483
  } catch {
444
484
  return null;
445
485
  }
446
486
  }
447
487
  function harnessesReading(dir, enabled) {
448
- return enabled.filter((id) => HARNESSES.find((h) => h.id === id)?.globalDirs().includes(dir));
488
+ const real = realPath(dir);
489
+ return enabled.filter((id) => HARNESSES.find((h) => h.id === id)?.globalDirs().some((d) => realPath(d) === real));
449
490
  }
450
491
  var systemDirs = () => [codexSystemDir()];
451
492
  function globalSkillDirs() {
452
- return [...new Set(HARNESSES.flatMap((h) => h.globalDirs()))].filter((d) => !systemDirs().includes(d));
493
+ const seen = /* @__PURE__ */ new Set();
494
+ return [...new Set(HARNESSES.flatMap((h) => h.globalDirs()))].filter((d) => {
495
+ if (systemDirs().includes(d)) return false;
496
+ const real = realPath(d);
497
+ if (seen.has(real)) return false;
498
+ seen.add(real);
499
+ return true;
500
+ });
501
+ }
502
+ function globalDirAs(dir) {
503
+ const real = realPath(dir);
504
+ return globalSkillDirs().find((d) => realPath(d) === real) ?? dir;
453
505
  }
454
506
  function systemSkills(enabled) {
455
507
  return systemDirs().flatMap(
@@ -492,7 +544,7 @@ function globalFolderSkills(enabled) {
492
544
  const inAgents = e.dir === agentsDir || target_.includes(".agents/skills");
493
545
  const locked = lock[e.name]?.source;
494
546
  if (existsSync6(join7(e.path, OWN_SKILL_MARKER))) {
495
- skills.push({ name: e.name, kind: "built-in", origin: "skilllib", path: e.path, description: readSkillInfo(e.path).description, movable: false, broken: false, harnesses: harnessesReading(e.dir, enabled), links: [] });
547
+ skills.push({ name: e.name, kind: "skilllib", origin: "skilllib", path: e.path, description: readSkillInfo(e.path).description, movable: false, broken: false, harnesses: harnessesReading(e.dir, enabled), links: [] });
496
548
  continue;
497
549
  }
498
550
  skills.push({
@@ -516,8 +568,21 @@ function globalFolderSkills(enabled) {
516
568
  return skills;
517
569
  }
518
570
  function tildify2(p) {
519
- const shown = p.startsWith(userHome()) ? "~" + p.slice(userHome().length) : p;
520
- return shown.replace(/\\/g, "/");
571
+ return tildify(p).replace(/\\/g, "/");
572
+ }
573
+ function grokBuiltInSkills(enabled) {
574
+ if (!enabled.includes("grok")) return [];
575
+ return skillDirsIn(join7(grokDir(), "bundled", "skills")).map((path) => ({
576
+ name: basename2(path),
577
+ kind: "built-in",
578
+ origin: "Grok",
579
+ path,
580
+ description: readSkillInfo(path).description,
581
+ movable: false,
582
+ broken: false,
583
+ harnesses: ["grok"],
584
+ links: []
585
+ }));
521
586
  }
522
587
  function cursorBuiltInSkills(enabled) {
523
588
  if (!enabled.includes("cursor")) return [];
@@ -534,30 +599,41 @@ function cursorBuiltInSkills(enabled) {
534
599
  }));
535
600
  }
536
601
  function claudeAiSkills(enabled) {
537
- const synced = join7(claudeDir(), "skills", "synced");
538
- const harnesses = harnessesReading(join7(claudeDir(), "skills"), enabled);
539
- if (!existsSync6(synced) || harnesses.length === 0) return [];
540
- return readdirSync3(synced).filter((d) => !d.startsWith(".")).flatMap(
541
- (bucket) => skillDirsIn(join7(synced, bucket)).map((path) => ({
542
- name: basename2(path),
543
- kind: "claude.ai",
544
- origin: "claude.ai account",
545
- path,
546
- description: readSkillInfo(path).description,
547
- movable: false,
548
- broken: false,
549
- harnesses,
550
- links: []
551
- }))
602
+ const byReal = /* @__PURE__ */ new Map();
603
+ for (const skills of [join7(claudeDir(), "skills"), join7(userHome(), ".claude", "skills")]) {
604
+ const synced = join7(skills, "synced");
605
+ const real = realpathOrNull(synced);
606
+ const readers = harnessesReading(skills, enabled);
607
+ if (!real || readers.length === 0) continue;
608
+ const entry = byReal.get(real) ?? { dir: synced, readers: [] };
609
+ for (const id of readers) if (!entry.readers.includes(id)) entry.readers.push(id);
610
+ byReal.set(real, entry);
611
+ }
612
+ return [...byReal.values()].flatMap(
613
+ ({ dir, readers }) => readdirSync3(dir).filter((d) => !d.startsWith(".")).flatMap(
614
+ (bucket) => skillDirsIn(join7(dir, bucket)).map((path) => ({
615
+ name: basename2(path),
616
+ kind: "claude.ai",
617
+ origin: "claude.ai account",
618
+ path,
619
+ description: readSkillInfo(path).description,
620
+ movable: false,
621
+ broken: false,
622
+ harnesses: enabled.filter((id) => readers.includes(id)),
623
+ links: []
624
+ }))
625
+ )
552
626
  );
553
627
  }
554
- function setPluginEnabled(id, on) {
555
- const file = join7(claudeDir(), "settings.json");
556
- const settings = existsSync6(file) ? readJson(file) : {};
557
- if (!settings) throw new Error(`${file} isn't valid JSON, so skilllib won't rewrite it`);
558
- settings.enabledPlugins = { ...settings.enabledPlugins, [id]: on };
559
- mkdirSync4(claudeDir(), { recursive: true });
560
- writeFileSync4(file, JSON.stringify(settings, null, 2) + "\n");
628
+ function pluginDirCandidates(id, installPaths) {
629
+ const [name = "", marketplace = ""] = id.split("@");
630
+ const plugins = join7(claudeDir(), "plugins");
631
+ return [
632
+ ...installPaths,
633
+ join7(plugins, "marketplaces", marketplace, "plugins", name),
634
+ join7(plugins, "marketplaces", marketplace, "external_plugins", name),
635
+ join7(plugins, "marketplaces", marketplace)
636
+ ];
561
637
  }
562
638
  function repoSettingsFiles(root) {
563
639
  return [join7(root, ".claude", "settings.json"), join7(root, ".claude", "settings.local.json")];
@@ -575,13 +651,7 @@ function pluginSkills(harnesses, root) {
575
651
  const user = enabledPlugins();
576
652
  const enabled = root ? enabledPlugins(root) : user;
577
653
  const fromMarketplaces = [.../* @__PURE__ */ new Set([...Object.keys(enabled), ...Object.keys(user)])].filter((id) => claude2 && enabled[id] || cursor && user[id]).flatMap((id) => {
578
- const [name = "", marketplace = ""] = id.split("@");
579
- const candidates = [
580
- ...(installed[id] ?? []).flatMap((i) => i.installPath ? [i.installPath] : []),
581
- join7(plugins, "marketplaces", marketplace, "plugins", name),
582
- join7(plugins, "marketplaces", marketplace, "external_plugins", name),
583
- join7(plugins, "marketplaces", marketplace)
584
- ];
654
+ const candidates = pluginDirCandidates(id, (installed[id] ?? []).flatMap((i) => i.installPath ? [i.installPath] : []));
585
655
  const dir = candidates.find((c) => existsSync6(join7(c, "skills")));
586
656
  if (!dir) return [];
587
657
  const inCursor = cursor && user[id] && (installed[id] ?? []).some((i) => i.scope === "user" && i.installPath === dir);
@@ -616,6 +686,7 @@ function machineSkills(enabled = enabledHarnesses()) {
616
686
  ...claudeAiSkills(enabled),
617
687
  ...pluginSkills(enabled),
618
688
  ...cursorBuiltInSkills(enabled),
689
+ ...grokBuiltInSkills(enabled),
619
690
  ...systemSkills(enabled)
620
691
  ].sort(byKindAndName);
621
692
  }
@@ -637,9 +708,9 @@ function recordOrigin(name, origin) {
637
708
  writeFileSync4(originsFile(), JSON.stringify({ ...origins, [name]: origin }, null, 2) + "\n");
638
709
  }
639
710
  function originFor(dir) {
640
- const match = machineSkills().find((s) => resolve3(s.path) === resolve3(dir));
711
+ const match = machineSkills().find((s) => resolve5(s.path) === resolve5(dir));
641
712
  if (match) return match.kind === "global" ? "global folder" : `${match.kind}: ${match.origin}`;
642
- const project = dir.split(/[\\/]\.(?:claude|agents)[\\/]skills[\\/]/)[0];
713
+ const project = dir.split(/[\\/]\.(?:claude|agents|grok|cursor|codex)[\\/]skills[\\/]/)[0];
643
714
  if (!project || project === dir) return dir;
644
715
  const locked = projectSkillsLock(project)[basename2(dir)]?.source;
645
716
  return locked ? `skills.sh: ${locked}` : `project: ${basename2(project)}`;
@@ -658,7 +729,7 @@ function librarySkillDir(name) {
658
729
  }
659
730
  function realpathOrNull2(path) {
660
731
  try {
661
- return realpathSync3(path);
732
+ return realpathSync4(path);
662
733
  } catch {
663
734
  return null;
664
735
  }
@@ -685,7 +756,7 @@ function visibilityOf(root, name, enabled = enabledHarnesses()) {
685
756
  }
686
757
  function dependencyDirs(root, name, dep) {
687
758
  const wanted = installDirs(enabledHarnesses());
688
- const untracked = ALL_PROJECT_DIRS.find((d) => existsSync7(join8(root, d, name, "SKILL.md")) && !isLink(join8(root, d, name)));
759
+ const untracked = ALL_PROJECT_DIRS.find((d) => d !== GROK_SKILLS_DIR && existsSync7(join8(root, d, name, "SKILL.md")) && !isLink(join8(root, d, name)));
689
760
  const primary = dep ? dep.dir ?? PROJECT_SKILLS_DIR : untracked ?? wanted[0];
690
761
  return [primary, .../* @__PURE__ */ new Set([...dep?.links ?? [], ...wanted.filter((d) => d !== primary)])];
691
762
  }
@@ -737,7 +808,7 @@ function projectStatus(root) {
737
808
  unmanaged.push({
738
809
  name,
739
810
  managed: false,
740
- state: unmanagedState(name, real, location === AGENTS_SKILLS_DIR, source),
811
+ state: unmanagedState(name, real, isRepoSkillLocation(location), source),
741
812
  version: null,
742
813
  latest: latestVersion(name)?.version ?? null,
743
814
  location,
@@ -757,10 +828,12 @@ function unmanagedState(name, real, committed, source) {
757
828
  return latest === null ? "local only" : sameAsLibrary ? "untracked copy of library skill" : "untracked, differs from library";
758
829
  }
759
830
  var NESTED_READERS = [
760
- [PROJECT_SKILLS_DIR, ["claude-code", "cursor"]],
761
- [AGENTS_SKILLS_DIR, ["codex", "cursor"]],
762
- [".cursor/skills", ["cursor"]],
763
- [".codex/skills", ["codex", "cursor"]]
831
+ [PROJECT_SKILLS_DIR, ["claude-code", "cursor", "grok"]],
832
+ [AGENTS_SKILLS_DIR, ["codex", "cursor", "grok"]],
833
+ [".cursor/skills", ["cursor", "grok"]],
834
+ [".codex/skills", ["codex", "cursor"]],
835
+ // Cursor reads nested .grok/skills too, but skilllib doesn't count it: that folder stays Grok's.
836
+ [".grok/skills", ["grok"]]
764
837
  ];
765
838
  var NESTED_SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "vendor", "target", "coverage"]);
766
839
  function nestedSkills(root, { depth = 3, enabled = enabledHarnesses() } = {}) {
@@ -782,7 +855,7 @@ function nestedSkills(root, { depth = 3, enabled = enabledHarnesses() } = {}) {
782
855
  found.push({
783
856
  name,
784
857
  managed: false,
785
- state: unmanagedState(name, path, skillsDir === AGENTS_SKILLS_DIR, void 0),
858
+ state: unmanagedState(name, path, isRepoSkillLocation(skillsDir), void 0),
786
859
  version: null,
787
860
  latest: latestVersion(name)?.version ?? null,
788
861
  location,
@@ -797,28 +870,53 @@ function nestedSkills(root, { depth = 3, enabled = enabledHarnesses() } = {}) {
797
870
  walk(root, depth);
798
871
  return found.sort((a, b) => a.location.localeCompare(b.location) || a.name.localeCompare(b.name));
799
872
  }
800
- function linkEverywhere(root, name, location, { allowTracked = false } = {}) {
873
+ function sharedByGit(root, location, name) {
874
+ const copy = `${location}/${name}`;
875
+ if (isGitTracked(root, copy)) return true;
876
+ if (!isGitTracked(root, location)) return false;
877
+ try {
878
+ execFileSync("git", ["check-ignore", "-q", "--no-index", "--", copy], { cwd: root, stdio: "ignore" });
879
+ return false;
880
+ } catch {
881
+ return true;
882
+ }
883
+ }
884
+ function linkEverywhere(root, name, location, { allowTracked = false, dryRun = false } = {}) {
801
885
  const enabled = enabledHarnesses();
802
- const result = { created: [], blocked: [] };
886
+ const result = { created: [], blocked: [], uncommitted: [] };
887
+ let shared;
803
888
  for (const id of enabled) {
804
889
  if (visibilityOf(root, name, [id])[0].paths > 0) continue;
805
890
  const dir = harness(id).projectDirs[0];
806
891
  const link2 = join8(root, dir, name);
807
- if (entryExists(link2)) continue;
808
- if (!allowTracked && isGitTracked(root, dir) && !readConfig().agentsDirOk?.includes(root)) {
809
- result.blocked.push(dir);
810
- continue;
892
+ if (entryExists(link2) || Object.values(result).some((dirs) => dirs.includes(dir))) continue;
893
+ if (isGitTracked(root, dir)) {
894
+ if (!(shared ??= sharedByGit(root, location, name))) {
895
+ if (!result.uncommitted.includes(dir)) result.uncommitted.push(dir);
896
+ continue;
897
+ }
898
+ if (!allowTracked && !readConfig().agentsDirOk?.includes(root)) {
899
+ if (!result.blocked.includes(dir)) result.blocked.push(dir);
900
+ continue;
901
+ }
902
+ }
903
+ if (!dryRun) {
904
+ mkdirSync5(join8(root, dir), { recursive: true });
905
+ linkDir(join8(root, location, name), link2);
811
906
  }
812
- mkdirSync5(join8(root, dir), { recursive: true });
813
- linkDir(join8(root, location, name), link2);
814
907
  result.created.push(dir);
815
908
  }
816
909
  return result;
817
910
  }
818
- function unlinkEverywhere(root, name) {
911
+ function unlinkEverywhere(root, name, removing) {
912
+ const gone = removing ? realpathOrNull2(removing) : null;
819
913
  return ALL_PROJECT_DIRS.filter((dir) => {
820
914
  const link2 = join8(root, dir, name);
821
915
  if (!isLink(link2)) return false;
916
+ if (dir === GROK_SKILLS_DIR) {
917
+ const target = realpathOrNull2(link2);
918
+ if (target !== null && target !== gone) return false;
919
+ }
822
920
  unlinkSync(link2);
823
921
  return true;
824
922
  });
@@ -826,7 +924,7 @@ function unlinkEverywhere(root, name) {
826
924
  function relinkDependency(root, name, { allowTracked = false } = {}) {
827
925
  const manifest = readManifest(root);
828
926
  const dep = manifest.skills[name];
829
- if (!dep) return { created: [], blocked: [] };
927
+ if (!dep) return { created: [], blocked: [], uncommitted: [] };
830
928
  const result = linkEverywhere(root, name, dep.dir ?? PROJECT_SKILLS_DIR, { allowTracked });
831
929
  if (result.created.length) {
832
930
  manifest.skills[name] = { ...dep, links: [.../* @__PURE__ */ new Set([...dep.links ?? [], ...result.created])] };
@@ -837,13 +935,15 @@ function relinkDependency(root, name, { allowTracked = false } = {}) {
837
935
  function linkAll(root, { allowTracked = false } = {}) {
838
936
  const linked = [];
839
937
  const blocked = /* @__PURE__ */ new Set();
938
+ const uncommitted = [];
840
939
  for (const skill of projectStatus(root)) {
841
940
  if (!skill.visibility.some((v) => v.paths === 0) || skill.state === "folder missing") continue;
842
941
  const res = skill.managed ? relinkDependency(root, skill.name, { allowTracked }) : linkEverywhere(root, skill.name, skill.location, { allowTracked });
843
942
  if (res.created.length) linked.push({ name: skill.name, into: res.created });
844
943
  for (const dir of res.blocked) blocked.add(dir);
944
+ if (res.uncommitted.length) uncommitted.push(skill.name);
845
945
  }
846
- return { linked, blocked: [...blocked] };
946
+ return { linked, blocked: [...blocked], uncommitted };
847
947
  }
848
948
  function addSkill(root, name, { force = false, version, allowTracked = false } = {}) {
849
949
  const target = version === void 0 ? latestVersion(name) : getVersion(name, version);
@@ -861,13 +961,17 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
861
961
  reason: recorded ? "has local edits (use --force to overwrite, or save them to the library first)" : `${primary}/${name} already exists with different content (use --force to replace it)`
862
962
  };
863
963
  }
964
+ let backedUp;
864
965
  if (local !== target.hash || isLink(to)) {
865
966
  mkdirSync5(join8(root, primary), { recursive: true });
866
967
  if (isLink(to)) unlinkSync(to);
968
+ else if (local !== null && !knownContent) backedUp = stash(to, "edit-backup");
867
969
  copySkill(versionDir(name, target.version), to);
868
970
  }
869
971
  const links = [];
870
972
  const blocked = [];
973
+ const uncommitted = [];
974
+ let shared;
871
975
  for (const dir of linkDirs) {
872
976
  const link2 = join8(root, dir, name);
873
977
  if (isLink(link2) && realpathOrNull2(link2) === realpathOrNull2(to)) {
@@ -875,9 +979,15 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
875
979
  continue;
876
980
  }
877
981
  if (entryExists(link2)) continue;
878
- if (!allowTracked && !recorded?.links?.includes(dir) && isGitTracked(root, dir) && !readConfig().agentsDirOk?.includes(root)) {
879
- blocked.push(dir);
880
- continue;
982
+ if (!recorded?.links?.includes(dir) && isGitTracked(root, dir)) {
983
+ if (!(shared ??= sharedByGit(root, primary, name))) {
984
+ uncommitted.push(dir);
985
+ continue;
986
+ }
987
+ if (!allowTracked && !readConfig().agentsDirOk?.includes(root)) {
988
+ blocked.push(dir);
989
+ continue;
990
+ }
881
991
  }
882
992
  mkdirSync5(join8(root, dir), { recursive: true });
883
993
  linkDir(to, link2);
@@ -892,10 +1002,12 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
892
1002
  writeManifest(root, manifest);
893
1003
  return {
894
1004
  name,
895
- action: recorded || local !== null ? "updated" : "installed",
1005
+ action: !recorded && local === null ? "installed" : backedUp && recorded?.version === target.version ? "reset" : "updated",
896
1006
  ...recorded ? { from: recorded.version } : {},
897
1007
  to: target.version,
898
- ...blocked.length ? { blocked } : {}
1008
+ ...blocked.length ? { blocked } : {},
1009
+ ...uncommitted.length ? { uncommitted } : {},
1010
+ ...backedUp ? { backedUp } : {}
899
1011
  };
900
1012
  }
901
1013
  function removeSkill(root, name, { force = false } = {}) {
@@ -904,17 +1016,17 @@ function removeSkill(root, name, { force = false } = {}) {
904
1016
  if (!recorded) return { name, action: "skipped", reason: "not managed by skilllib in this project" };
905
1017
  const dir = join8(root, recorded.dir ?? PROJECT_SKILLS_DIR, name);
906
1018
  const local = treeHash(dir);
907
- if (local !== null && local !== recorded.hash && !versionForHash(name, local) && !force) {
908
- return { name, action: "skipped", reason: "has local edits (use --force to delete anyway)" };
909
- }
1019
+ const edited = local !== null && local !== recorded.hash && !versionForHash(name, local);
1020
+ if (edited && !force) return { name, action: "skipped", reason: "has local edits (use --force to delete anyway)" };
910
1021
  for (const linkDir2 of recorded.links ?? []) {
911
1022
  const link2 = join8(root, linkDir2, name);
912
1023
  if (isLink(link2)) unlinkSync(link2);
913
1024
  }
914
- rmSync2(dir, { recursive: true, force: true });
1025
+ const backedUp = edited ? stash(dir, "edit-backup") : void 0;
1026
+ if (!backedUp) rmSync2(dir, { recursive: true, force: true });
915
1027
  delete manifest.skills[name];
916
1028
  writeManifest(root, manifest);
917
- return { name, action: "removed" };
1029
+ return { name, action: "removed", ...backedUp ? { backedUp } : {} };
918
1030
  }
919
1031
  function syncProject(root, { force = false } = {}) {
920
1032
  const { skills } = readManifest(root);
@@ -925,7 +1037,8 @@ function syncProject(root, { force = false } = {}) {
925
1037
  const pinned = dep.version > 0 && getVersion(name, dep.version) ? dep.version : void 0;
926
1038
  if (local !== null && local !== dep.hash && !force) return [{ name, action: "skipped", reason: "has local edits (use --force to reset them)" }];
927
1039
  const change = addSkill(root, name, { force: true, version: pinned });
928
- return change.action === "skipped" ? [change] : [{ ...change, action: local === null ? "installed" : "updated" }];
1040
+ 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` }];
1041
+ return [{ ...change, action: local === null ? "installed" : change.action }];
929
1042
  });
930
1043
  }
931
1044
  function discardEdits(root, name) {
@@ -944,6 +1057,7 @@ function updateProject(root, names, { force = false } = {}) {
944
1057
  const dep = skills[name];
945
1058
  const local = treeHash(join8(root, dep.dir ?? PROJECT_SKILLS_DIR, name));
946
1059
  if (dep.hash === latest.hash && local === latest.hash) return [];
1060
+ if (dep.hash === latest.hash && local !== null && !force) return [];
947
1061
  const change = addSkill(root, name, { force });
948
1062
  return change.action === "skipped" || change.from !== change.to || local !== latest.hash ? [change] : [];
949
1063
  });
@@ -985,7 +1099,7 @@ function stash(dir, bucket) {
985
1099
  return target;
986
1100
  }
987
1101
  function removeUntracked(root, name, path) {
988
- unlinkEverywhere(root, name);
1102
+ unlinkEverywhere(root, name, path);
989
1103
  return stash(path, "trash");
990
1104
  }
991
1105
  function projectsUsing(name) {
@@ -1038,12 +1152,12 @@ function createSkill(name, description) {
1038
1152
  join8(dir, "SKILL.md"),
1039
1153
  `---
1040
1154
  name: ${name}
1041
- description: ${description || "Describe when Claude should use this skill."}
1155
+ description: ${description || "Describe when an agent should use this skill."}
1042
1156
  ---
1043
1157
 
1044
1158
  # ${name}
1045
1159
 
1046
- Instructions for Claude go here.
1160
+ Instructions for the agent go here.
1047
1161
  `
1048
1162
  );
1049
1163
  forgetLatest(name);
@@ -1061,7 +1175,11 @@ function listBackups() {
1061
1175
  const from = index[entry] ?? (kind === "trash" ? librarySkillDir(name) : join8(claudeDir(), "skills", name));
1062
1176
  return [{ name, kind, path: join8(dir, entry), movedAt: `${match[2]} ${match[3]}:${match[4]}`, from }];
1063
1177
  });
1064
- }).sort((a, b) => b.movedAt.localeCompare(a.movedAt));
1178
+ }).sort((a, b) => b.path.slice(-24).localeCompare(a.path.slice(-24)));
1179
+ }
1180
+ function backupFrom(backup) {
1181
+ if (backup.kind === "plugin") return "Claude Code plugin";
1182
+ return backup.from === librarySkillDir(backup.name) ? "library (deleted)" : tildify(dirname2(backup.from));
1065
1183
  }
1066
1184
  function restoreBackup(backup) {
1067
1185
  const to = backup.from;
@@ -1114,7 +1232,7 @@ function installAgentSkill(enabled = enabledHarnesses()) {
1114
1232
  const [primary, ...links] = agentSkillDirs(enabled);
1115
1233
  if (!primary) return { ok: false, reason: "no agents chosen yet (skilllib harnesses <id>...)" };
1116
1234
  const foreign = [primary, ...links].find((d) => entryExists2(d) && !isOurs(d) && !isDangling(d));
1117
- if (foreign) return { ok: false, reason: `${foreign} already exists and isn't skilllib's` };
1235
+ if (foreign) return { ok: false, reason: `${tildify(foreign)} already exists and isn't skilllib's` };
1118
1236
  for (const d of [primary, ...links]) if (isDangling(d)) unlinkSync2(d);
1119
1237
  mkdirSync6(primary, { recursive: true });
1120
1238
  writeFileSync6(join9(primary, "SKILL.md"), shippedSkill());
@@ -1143,12 +1261,8 @@ function refreshAgentSkill() {
1143
1261
  }
1144
1262
 
1145
1263
  // src/commands.ts
1146
- import { existsSync as existsSync14, readFileSync as readFileSync9 } from "fs";
1147
- import { basename as basename8, dirname as dirname5, join as join15, resolve as resolve4, sep as sep5 } from "path";
1148
-
1149
- // src/here.ts
1150
- import { existsSync as existsSync12, readdirSync as readdirSync7 } from "fs";
1151
- import { basename as basename7, join as join13 } from "path";
1264
+ import { existsSync as existsSync13, readFileSync as readFileSync9 } from "fs";
1265
+ import { basename as basename8, join as join14, resolve as resolve7, sep as sep5 } from "path";
1152
1266
 
1153
1267
  // src/git.ts
1154
1268
  import { execFileSync as execFileSync2 } from "child_process";
@@ -1162,9 +1276,9 @@ function git(root, args, input) {
1162
1276
  return typeof out2 === "string" ? out2 : null;
1163
1277
  }
1164
1278
  }
1165
- function gitInfo(root) {
1279
+ function gitInfo(root, extraDirs = []) {
1166
1280
  if (!existsSync9(join10(root, ".git"))) return null;
1167
- const dirs = [...ALL_PROJECT_DIRS, MANIFEST_FILE].filter((d) => existsSync9(join10(root, d)));
1281
+ const dirs = [...ALL_PROJECT_DIRS, MANIFEST_FILE, ...extraDirs].filter((d) => existsSync9(join10(root, d)));
1168
1282
  if (dirs.length === 0) return { of: () => "new" };
1169
1283
  const tracked = (git(root, ["ls-files", "-z", "--", ...dirs]) ?? "").split("\0").filter(Boolean);
1170
1284
  const dirty = (git(root, ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", ...dirs]) ?? "").split("\0").filter(Boolean).map((entry) => entry.slice(3));
@@ -1189,14 +1303,18 @@ function relativeTo(root, path) {
1189
1303
  return relative3(root, path).split("\\").join("/");
1190
1304
  }
1191
1305
 
1306
+ // src/here.ts
1307
+ import { readdirSync as readdirSync7 } from "fs";
1308
+ import { basename as basename7 } from "path";
1309
+
1192
1310
  // src/health.ts
1193
1311
  import { lstatSync as lstatSync4 } from "fs";
1194
- import { basename as basename6 } from "path";
1312
+ import { basename as basename6, dirname as dirname5 } from "path";
1195
1313
 
1196
1314
  // src/plugins.ts
1197
1315
  import { execFileSync as execFileSync3 } from "child_process";
1198
- import { existsSync as existsSync10, mkdirSync as mkdirSync7, readdirSync as readdirSync5, readFileSync as readFileSync8, statSync as statSync2, writeFileSync as writeFileSync7 } from "fs";
1199
- import { basename as basename4, dirname as dirname3, join as join11 } from "path";
1316
+ import { existsSync as existsSync10, mkdirSync as mkdirSync7, readdirSync as readdirSync5, readFileSync as readFileSync8, statSync as statSync3, writeFileSync as writeFileSync7 } from "fs";
1317
+ import { basename as basename4, dirname as dirname3, join as join11, resolve as resolve6 } from "path";
1200
1318
  function readJson2(path) {
1201
1319
  try {
1202
1320
  return JSON.parse(readFileSync8(path, "utf-8"));
@@ -1239,22 +1357,109 @@ function claudePlugins(machine, root) {
1239
1357
  };
1240
1358
  });
1241
1359
  }
1360
+ function newer(a, b) {
1361
+ const parts = (v) => v.replace(/^v/i, "").split(/[.+-]/).map((x) => parseInt(x, 10) || 0);
1362
+ const pa = parts(a);
1363
+ const pb = parts(b);
1364
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) > (pb[i] ?? 0);
1365
+ return false;
1366
+ }
1367
+ function updateOf(name, marketplace, entry, catalogs) {
1368
+ const dir = join11(claudeDir(), "plugins", "marketplaces", marketplace);
1369
+ if (!catalogs.has(marketplace)) catalogs.set(marketplace, readJson2(join11(dir, ".claude-plugin", "marketplace.json"))?.plugins ?? []);
1370
+ const listed = catalogs.get(marketplace).find((p) => p.name === name);
1371
+ if (!listed) return void 0;
1372
+ const version = listed.version ?? (typeof listed.source === "string" ? readJson2(join11(dir, listed.source, ".claude-plugin", "plugin.json"))?.version : void 0);
1373
+ if (version && entry.version) return newer(version, entry.version) ? version : void 0;
1374
+ const sha = typeof listed.source === "object" ? listed.source?.sha : void 0;
1375
+ return sha && entry.gitCommitSha && sha !== entry.gitCommitSha ? "newer" : void 0;
1376
+ }
1377
+ function pluginRoot(id, entry) {
1378
+ const candidates = pluginDirCandidates(id, entry.installPath ? [entry.installPath] : []);
1379
+ return candidates.find((c) => existsSync10(join11(c, "skills"))) ?? candidates.find((c) => existsSync10(c)) ?? candidates[0];
1380
+ }
1381
+ function describe(root) {
1382
+ const manifest = readJson2(join11(root, ".claude-plugin", "plugin.json")) ?? {};
1383
+ return { description: manifest.description ?? "", version: manifest.version, skills: skillDirsIn(join11(root, "skills")), extras: extrasOf(root) };
1384
+ }
1385
+ function installedPlugins() {
1386
+ const plugins = join11(claudeDir(), "plugins");
1387
+ const installed = readJson2(join11(plugins, "installed_plugins.json"))?.plugins ?? {};
1388
+ const catalogs = /* @__PURE__ */ new Map();
1389
+ const fromMarketplaces = Object.entries(installed).flatMap(
1390
+ ([id, entries]) => entries.map((entry) => {
1391
+ const [name = id, marketplace = ""] = id.split("@");
1392
+ const scope = entry.scope ?? "user";
1393
+ const projectPath = scope === "user" ? void 0 : entry.projectPath;
1394
+ const root = pluginRoot(id, entry);
1395
+ const about = describe(root);
1396
+ const update2 = updateOf(name, marketplace, entry, catalogs);
1397
+ return {
1398
+ id,
1399
+ scope,
1400
+ ...projectPath ? { projectPath } : {},
1401
+ synced: false,
1402
+ agent: "claude-code",
1403
+ on: enabledPlugins(projectPath)[id] === true,
1404
+ root,
1405
+ ...about,
1406
+ version: entry.version ?? about.version,
1407
+ ...update2 ? { update: update2 } : {}
1408
+ };
1409
+ })
1410
+ );
1411
+ const user = enabledPlugins();
1412
+ const fromSettings = Object.keys(user).filter((id) => user[id] === true && id.includes("@") && !id.endsWith("@synced") && !installed[id]).flatMap((id) => {
1413
+ const root = pluginDirCandidates(id, []).find((c) => existsSync10(join11(c, "skills")));
1414
+ return root ? [{ id, scope: "user", synced: false, agent: "claude-code", on: true, settingsOnly: true, root, ...describe(root) }] : [];
1415
+ });
1416
+ const syncedDir = join11(plugins, "synced");
1417
+ const dirs = (p) => existsSync10(p) ? readdirSync5(p, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".")).map((d) => join11(p, d.name)) : [];
1418
+ const fromSynced = dirs(syncedDir).flatMap(dirs).map((root) => {
1419
+ const id = `${readJson2(join11(root, ".claude-plugin", "plugin.json"))?.name ?? basename4(root)}@synced`;
1420
+ return { id, scope: null, synced: true, agent: "claude-code", on: user[id] !== false, root, ...describe(root) };
1421
+ });
1422
+ return [...fromMarketplaces, ...fromSettings, ...fromSynced];
1423
+ }
1424
+ function cursorPlugins() {
1425
+ const cache = join11(userHome(), ".cursor", "plugins", "cache");
1426
+ const dirs = (p) => existsSync10(p) ? readdirSync5(p).filter((d) => !d.startsWith(".") && statSync3(join11(p, d)).isDirectory()) : [];
1427
+ return dirs(cache).flatMap(
1428
+ (marketplace) => dirs(join11(cache, marketplace)).flatMap((plugin) => {
1429
+ const versions = dirs(join11(cache, marketplace, plugin)).map((v) => join11(cache, marketplace, plugin, v));
1430
+ const root = versions.sort((a, b) => statSync3(b).mtimeMs - statSync3(a).mtimeMs)[0];
1431
+ if (!root) return [];
1432
+ const about = describe(root);
1433
+ return [{ id: `${plugin}@${marketplace}`, scope: null, synced: false, agent: "cursor", on: null, root, ...about, version: about.version ?? basename4(root) }];
1434
+ })
1435
+ );
1436
+ }
1437
+ var binaries = /* @__PURE__ */ new Map();
1438
+ function forgetClaudeBinary() {
1439
+ binaries.clear();
1440
+ }
1242
1441
  function claudeBinary() {
1243
- const candidates = ["claude", join11(userHome(), ".local", "bin", "claude"), join11(userHome(), ".claude", "local", "claude")];
1244
- return candidates.find((bin) => {
1245
- try {
1246
- return execFileSync3(bin, ["--version"], { stdio: ["ignore", "pipe", "ignore"], timeout: 3e4, encoding: "utf-8" }).includes("Claude Code");
1247
- } catch {
1248
- return false;
1249
- }
1250
- }) ?? null;
1442
+ const key = `${process.env.PATH}|${userHome()}`;
1443
+ if (!binaries.has(key)) {
1444
+ const candidates = ["claude", join11(userHome(), ".local", "bin", "claude"), join11(userHome(), ".claude", "local", "claude")];
1445
+ const found = candidates.find((bin) => {
1446
+ try {
1447
+ return execFileSync3(bin, ["--version"], { stdio: ["ignore", "pipe", "ignore"], timeout: 1e4, encoding: "utf-8" }).includes("Claude Code");
1448
+ } catch {
1449
+ return false;
1450
+ }
1451
+ });
1452
+ binaries.set(key, found ?? null);
1453
+ }
1454
+ return binaries.get(key);
1251
1455
  }
1252
- function claude(args, cwd) {
1253
- const bin = claudeBinary();
1456
+ function claude(args, cwd, bin = claudeBinary()) {
1457
+ if (cwd && !existsSync10(cwd)) return { ok: false, reason: `${cwd} no longer exists; run \`claude ${args.join(" ")}\` in the project's new folder, or remove the install with /plugin` };
1254
1458
  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` };
1459
+ const slow = args[1] === "install" || args[1] === "update";
1255
1460
  try {
1256
- execFileSync3(bin, args, { cwd, stdio: ["ignore", "pipe", "pipe"], timeout: 12e4, encoding: "utf-8" });
1257
- return { ok: true };
1461
+ const out2 = execFileSync3(bin, args, { cwd, stdio: ["ignore", "pipe", "pipe"], timeout: slow ? 12e4 : 2e4, encoding: "utf-8" });
1462
+ return { ok: true, out: out2 };
1258
1463
  } catch (err) {
1259
1464
  const e = err;
1260
1465
  return { ok: false, reason: `${(e.stderr || e.stdout || e.message || "failed").trim().split("\n").slice(-1)[0]} (ran ${bin})` };
@@ -1268,12 +1473,88 @@ function setPluginIn(file, id, on) {
1268
1473
  writeFileSync7(file, JSON.stringify({ ...settings, enabledPlugins: { ...enabled, [id]: on } }, null, 2) + "\n");
1269
1474
  return { ok: true };
1270
1475
  }
1271
- function turnOffSynced(id) {
1272
- return setPluginIn(join11(claudeDir(), "settings.json"), id, false);
1476
+ function ignoreLocalSettings(root) {
1477
+ const file = ".claude/settings.local.json";
1478
+ const git2 = (...args) => execFileSync3("git", args, { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1479
+ try {
1480
+ git2("rev-parse", "--git-dir");
1481
+ } catch {
1482
+ return "";
1483
+ }
1484
+ try {
1485
+ git2("ls-files", "--error-unmatch", "--", file);
1486
+ return "; git tracks that file, so this shows in git status";
1487
+ } catch {
1488
+ }
1489
+ try {
1490
+ git2("check-ignore", "-q", "--", file);
1491
+ return ", which git ignores";
1492
+ } catch {
1493
+ }
1494
+ try {
1495
+ const exclude = resolve6(root, git2("rev-parse", "--git-path", "info/exclude"));
1496
+ const before = existsSync10(exclude) ? readFileSync8(exclude, "utf-8") : "";
1497
+ mkdirSync7(dirname3(exclude), { recursive: true });
1498
+ writeFileSync7(exclude, `${before}${before && !before.endsWith("\n") ? "\n" : ""}/${git2("rev-parse", "--show-prefix")}${file}
1499
+ `);
1500
+ return ", which git now ignores via .git/info/exclude";
1501
+ } catch {
1502
+ return "; git doesn't ignore that file yet: it's local only, don't commit it";
1503
+ }
1273
1504
  }
1274
1505
  function turnOffIn(plugin, root) {
1275
- const r = plugin.synced ? setPluginIn(join11(root, ".claude", "settings.local.json"), plugin.id, false) : claude(["plugin", "disable", plugin.id, "--scope", "local"], root);
1276
- return r.ok ? { ok: true, message: `${plugin.id} turned off in ${basename4(root)}; other repos keep it` } : { ok: false, message: `${plugin.id}: ${r.reason}` };
1506
+ const done = `${plugin.id} turned off in ${basename4(root)}; other repos keep it`;
1507
+ const bin = plugin.synced ? null : claudeBinary();
1508
+ if (!bin) {
1509
+ const r2 = setPluginIn(join11(root, ".claude", "settings.local.json"), plugin.id, false);
1510
+ return r2.ok ? { ok: true, message: `${done} (in .claude/settings.local.json${ignoreLocalSettings(root)})` } : { ok: false, message: `${plugin.id}: ${r2.reason}` };
1511
+ }
1512
+ const r = claude(["plugin", "disable", plugin.id, "--scope", "local"], root, bin);
1513
+ return r.ok ? { ok: true, message: done } : { ok: false, message: `${plugin.id}: ${r.reason}` };
1514
+ }
1515
+ function settingsFileOf(p) {
1516
+ if (p.synced || !p.scope || p.scope === "user") return join11(claudeDir(), "settings.json");
1517
+ if (!p.projectPath) return null;
1518
+ return join11(p.projectPath, ".claude", p.scope === "local" ? "settings.local.json" : "settings.json");
1519
+ }
1520
+ function whereOf(p) {
1521
+ return p.projectPath ? ` in ${basename4(p.projectPath)} (${p.scope})` : "";
1522
+ }
1523
+ function setPluginOn(p, on) {
1524
+ const done = { ok: true, message: `${p.id} is ${on ? "on" : "off"}${whereOf(p)}${p.synced ? "" : "; Claude Code picks it up in its next session"}` };
1525
+ if (p.scope && p.scope !== "user" && !p.projectPath) return { ok: false, message: `${p.id}: skilllib doesn't know which project it's installed in; run \`claude plugin ${on ? "enable" : "disable"} ${p.id} --scope ${p.scope}\` there` };
1526
+ const bin = p.synced || p.settingsOnly ? null : claudeBinary();
1527
+ if (bin) {
1528
+ const r2 = claude(["plugin", on ? "enable" : "disable", p.id, ...p.scope ? ["--scope", p.scope] : []], p.projectPath, bin);
1529
+ return r2.ok ? done : { ok: false, message: `${p.id}: ${r2.reason}` };
1530
+ }
1531
+ const r = setPluginIn(settingsFileOf(p), p.id, on);
1532
+ if (!r.ok) return { ok: false, message: `${p.id}: ${r.reason}` };
1533
+ return p.scope === "local" && p.projectPath ? { ...done, message: done.message + ignoreLocalSettings(p.projectPath) } : done;
1534
+ }
1535
+ function uninstallPlugin(p) {
1536
+ if (p.synced) return { ok: false, message: `${p.id} is synced from your claude.ai account: remove it there, or turn it off here` };
1537
+ if (p.settingsOnly) return { ok: false, message: `${p.id} isn't installed, only turned on in your settings: turn it off instead` };
1538
+ if (p.scope && p.scope !== "user" && !p.projectPath) return { ok: false, message: `${p.id}: skilllib doesn't know which project it's installed in; run \`claude plugin uninstall ${p.id} --scope ${p.scope}\` there` };
1539
+ const wasOn = enabledPlugins(p.projectPath)[p.id] === true;
1540
+ const r = claude(["plugin", "uninstall", p.id, "--keep-data", "--scope", p.scope ?? "user"], p.projectPath);
1541
+ if (!r.ok) return { ok: false, message: `${p.id}: ${r.reason}` };
1542
+ recordRemovedPlugin(p.id, p.scope ?? "user", p.projectPath, wasOn);
1543
+ return { ok: true, message: `${p.id} is uninstalled${whereOf(p)} (Settings \u203A Backups reinstalls it)` };
1544
+ }
1545
+ function updatePlugin(p) {
1546
+ if (p.synced) return { ok: false, message: `${p.id} is synced from your claude.ai account: it updates from there` };
1547
+ if (p.settingsOnly) return { ok: false, message: `${p.id} isn't installed, only turned on in your settings: install it with /plugin to update it` };
1548
+ const r = claude(["plugin", "update", p.id, ...p.scope ? ["--scope", p.scope] : [], "--json"], p.projectPath);
1549
+ if (!r.ok) return { ok: false, message: `${p.id}: ${r.reason}; to update it yourself, run \`claude plugin update ${p.id}\`${p.projectPath ? ` in ${p.projectPath}` : ""}` };
1550
+ let result = {};
1551
+ try {
1552
+ result = JSON.parse(r.out.trim().split("\n").pop() ?? "{}");
1553
+ } catch {
1554
+ }
1555
+ if (result.updateOutcome === "up_to_date") return { ok: true, message: `${p.id} is already the newest version${result.newVersion ? ` (${result.newVersion})` : ""}${whereOf(p)}` };
1556
+ const versions = result.oldVersion && result.newVersion ? ` from ${result.oldVersion} to ${result.newVersion}` : "";
1557
+ return { ok: true, message: `${p.id} is updated${versions}${whereOf(p)}; Claude Code uses the new version after a restart` };
1277
1558
  }
1278
1559
  function removedFile() {
1279
1560
  return join11(skilllibHome(), "plugin-backup.json");
@@ -1287,42 +1568,45 @@ function removePlugin(plugin, how) {
1287
1568
  }
1288
1569
  const imported = plugin.skills.filter((dir) => latestVersion(basename4(dir)) === null).map((dir) => importSkill(dir).name);
1289
1570
  const scope = plugin.scope ? ["--scope", plugin.scope] : [];
1290
- const r = plugin.synced ? turnOffSynced(plugin.id) : claude(["plugin", how === "delete" ? "uninstall" : "disable", plugin.id, ...scope], plugin.projectPath);
1571
+ const r = plugin.synced ? (
1572
+ // Synced plugins aren't installed, so `claude plugin` can't touch them: their switch is enabledPlugins.
1573
+ setPluginIn(join11(claudeDir(), "settings.json"), plugin.id, false)
1574
+ ) : claude(["plugin", how === "delete" ? "uninstall" : "disable", plugin.id, ...scope], plugin.projectPath);
1291
1575
  const saved = imported.length ? `; ${imported.join(", ")} copied into Your skills` : "";
1292
1576
  if (!r.ok) return { ok: false, message: `${plugin.id}: ${r.reason}${saved}` };
1293
- if (how === "delete" && !plugin.synced) recordRemovedPlugin(plugin.id, plugin.scope ?? "user");
1577
+ if (how === "delete" && !plugin.synced) recordRemovedPlugin(plugin.id, plugin.scope ?? "user", plugin.projectPath, true);
1294
1578
  return { ok: true, message: `${plugin.id} ${how === "delete" && !plugin.synced ? "removed" : "turned off"}${saved}` };
1295
1579
  }
1296
- function recordRemovedPlugin(id, scope) {
1580
+ function recordRemovedPlugin(id, scope, projectPath, on = true) {
1297
1581
  const list2 = readJson2(removedFile()) ?? [];
1298
1582
  mkdirSync7(skilllibHome(), { recursive: true });
1299
- writeFileSync7(removedFile(), JSON.stringify([...list2, { id, scope, at: (/* @__PURE__ */ new Date()).toISOString() }], null, 2) + "\n");
1583
+ writeFileSync7(removedFile(), JSON.stringify([...list2, { id, scope, ...projectPath ? { projectPath } : {}, on, at: (/* @__PURE__ */ new Date()).toISOString() }], null, 2) + "\n");
1300
1584
  }
1301
1585
  function pluginBackups() {
1302
1586
  return (readJson2(removedFile()) ?? []).map((r) => ({
1303
1587
  name: r.id,
1304
1588
  kind: "plugin",
1305
- path: r.scope,
1589
+ // Project and local installs go back into their project.
1590
+ path: r.projectPath ? `${r.scope}:${r.projectPath}` : r.scope,
1306
1591
  movedAt: r.at.slice(0, 16).replace("T", " "),
1307
1592
  from: r.id
1308
1593
  }));
1309
1594
  }
1310
1595
  function restorePlugin(backup) {
1311
- const r = claude(["plugin", "install", backup.from, "--scope", backup.path]);
1596
+ const [scope = "user", ...rest] = backup.path.split(":");
1597
+ const projectPath = rest.join(":") || void 0;
1598
+ const r = claude(["plugin", "install", backup.from, "--scope", scope], projectPath);
1312
1599
  if (!r.ok) return r;
1313
- writeFileSync7(removedFile(), JSON.stringify((readJson2(removedFile()) ?? []).filter((x) => x.id !== backup.from), null, 2) + "\n");
1314
- return { ok: true, to: `Claude Code plugins (${backup.path})` };
1600
+ const records = readJson2(removedFile()) ?? [];
1601
+ const same = (x) => x.id === backup.from && (x.projectPath ?? void 0) === projectPath && x.scope === scope;
1602
+ const wasOff = records.filter(same).pop()?.on === false;
1603
+ const off = wasOff ? claude(["plugin", "disable", backup.from, "--scope", scope], projectPath) : null;
1604
+ writeFileSync7(removedFile(), JSON.stringify(records.filter((x) => !same(x)), null, 2) + "\n");
1605
+ const where = `Claude Code plugins (${scope}${projectPath ? ` in ${basename4(projectPath)}` : ""}${wasOff ? off?.ok ? ", off as it was" : ", but it's on: turn it off from Plugins" : ""})`;
1606
+ return { ok: true, to: where };
1315
1607
  }
1316
- function cursorPluginSkills() {
1317
- const cache = join11(userHome(), ".cursor", "plugins", "cache");
1318
- const dirs = (p) => existsSync10(p) ? readdirSync5(p).filter((d) => !d.startsWith(".") && statSync2(join11(p, d)).isDirectory()) : [];
1319
- return dirs(cache).flatMap(
1320
- (marketplace) => dirs(join11(cache, marketplace)).flatMap((plugin) => {
1321
- const versions = dirs(join11(cache, marketplace, plugin)).map((v) => join11(cache, marketplace, plugin, v));
1322
- const newest = versions.sort((a, b) => statSync2(b).mtimeMs - statSync2(a).mtimeMs)[0];
1323
- return newest ? skillDirsIn(join11(newest, "skills")).map((path) => ({ name: basename4(path), plugin, path })) : [];
1324
- })
1325
- );
1608
+ function cursorPluginSkills(plugins = cursorPlugins()) {
1609
+ return plugins.flatMap((p) => p.skills.map((path) => ({ name: basename4(path), plugin: p.id.split("@")[0], path })));
1326
1610
  }
1327
1611
 
1328
1612
  // src/tidy.ts
@@ -1350,6 +1634,11 @@ function namesIn(dirs) {
1350
1634
  }
1351
1635
  return [...names].sort();
1352
1636
  }
1637
+ var PROJECT_TIDY_DIRS = ALL_PROJECT_DIRS.filter((d) => d !== GROK_SKILLS_DIR);
1638
+ function globalTidyDirs() {
1639
+ const own = realPath(join12(grokDir(), "skills"));
1640
+ return globalSkillDirs().filter((d) => realPath(d) !== own);
1641
+ }
1353
1642
  function runsFrom(id, dirsWithCopy, readsOf) {
1354
1643
  const reads = readsOf(id).filter((d) => dirsWithCopy.includes(d));
1355
1644
  if (id === "codex") return reads;
@@ -1372,8 +1661,8 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
1372
1661
  const readsOf = (id) => HARNESSES.find((h) => h.id === id).projectDirs.map(abs);
1373
1662
  let git2;
1374
1663
  const skillsLock = projectSkillsLock(root);
1375
- for (const name of namesIn(ALL_PROJECT_DIRS.map(abs))) {
1376
- const entries = entriesOf(ALL_PROJECT_DIRS.map(abs), name);
1664
+ for (const name of namesIn(PROJECT_TIDY_DIRS.map(abs))) {
1665
+ const entries = entriesOf(PROJECT_TIDY_DIRS.map(abs), name);
1377
1666
  const external = entries.find((e) => e.link && e.real && !(e.real + sep4).startsWith(realpathOrNull2(root) + sep4));
1378
1667
  if (external) {
1379
1668
  report.skipped.push({ name, reason: `${rel(external.path)} links outside the repo` });
@@ -1388,7 +1677,7 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
1388
1677
  if (dep) {
1389
1678
  primary = reals.find((r) => rel(r.dir) === (dep.dir ?? PROJECT_SKILLS_DIR));
1390
1679
  if (!primary) continue;
1391
- } else if (chosen) {
1680
+ } else if (chosen && reals.some((r) => r.dir === chosen)) {
1392
1681
  primary = reals.find((r) => r.dir === chosen);
1393
1682
  } else {
1394
1683
  if (git2 === void 0) git2 = gitInfo(root);
@@ -1412,7 +1701,8 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
1412
1701
  if (!needed) steps.push({ kind: "unlink", path: e.path });
1413
1702
  } else steps.push(needed ? { kind: "link", path: e.path, target: primary.path } : { kind: "unlink", path: e.path });
1414
1703
  } else {
1415
- steps.push(needed ? { kind: "replace", path: e.path, target: primary.path } : { kind: "remove", path: e.path });
1704
+ const differs = e.hash !== primary.hash ? { differs: true } : {};
1705
+ steps.push(needed ? { kind: "replace", path: e.path, target: primary.path, ...differs } : { kind: "remove", path: e.path, ...differs });
1416
1706
  }
1417
1707
  }
1418
1708
  if (steps.length) report.plans.push({ name, root, primary: primary.path, steps });
@@ -1420,14 +1710,14 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
1420
1710
  return report;
1421
1711
  }
1422
1712
  function globalReadsOf(id) {
1423
- return HARNESSES.find((h) => h.id === id).globalDirs();
1713
+ return HARNESSES.find((h) => h.id === id).globalDirs().map(globalDirAs);
1424
1714
  }
1425
1715
  function isSkillsDirPlugin(path) {
1426
1716
  return existsSync11(join12(path, ".claude-plugin", "plugin.json"));
1427
1717
  }
1428
1718
  function planGlobalTidy({ keep = {}, enabled = enabledHarnesses() } = {}) {
1429
1719
  const report = { plans: [], conflicts: [], skipped: [] };
1430
- const dirs = globalSkillDirs();
1720
+ const dirs = globalTidyDirs();
1431
1721
  const agents = join12(userHome(), ".agents", "skills");
1432
1722
  for (const name of namesIn(dirs)) {
1433
1723
  const entries = entriesOf(dirs, name);
@@ -1435,15 +1725,16 @@ function planGlobalTidy({ keep = {}, enabled = enabledHarnesses() } = {}) {
1435
1725
  report.skipped.push({ name, reason: "a Claude Code plugin kept in a skills folder" });
1436
1726
  continue;
1437
1727
  }
1438
- const reals = entries.filter((e) => !e.link);
1728
+ const reals = entries.filter((e, i, all) => !e.link && all.findIndex((o) => !o.link && realpathOrNull2(o.path) === realpathOrNull2(e.path)) === i);
1439
1729
  if (reals.length < 2) continue;
1440
1730
  hashReals(reals);
1441
- const primary = reals.find((r) => r.dir === keep[name]) ?? reals.find((r) => r.dir === agents) ?? reals[0];
1442
- if (!keep[name] && reals.some((r) => r.hash !== primary.hash)) {
1731
+ const kept = reals.find((r) => r.dir === keep[name]);
1732
+ const primary = kept ?? reals.find((r) => r.dir === agents) ?? reals[0];
1733
+ if (!kept && reals.some((r) => r.hash !== primary.hash)) {
1443
1734
  report.conflicts.push(conflictOf(name, null, reals, enabled, globalReadsOf));
1444
1735
  continue;
1445
1736
  }
1446
- const steps = reals.filter((r) => r !== primary).map((r) => ({ kind: "replace", path: r.path, target: primary.path }));
1737
+ const steps = reals.filter((r) => r !== primary).map((r) => ({ kind: "replace", path: r.path, target: primary.path, ...r.hash !== primary.hash ? { differs: true } : {} }));
1447
1738
  report.plans.push({ name, root: null, primary: primary.path, steps });
1448
1739
  }
1449
1740
  return report;
@@ -1481,20 +1772,27 @@ function applyTidy(plan, { git: git2 = "keep", info: info2 } = {}) {
1481
1772
  return { name: plan.name, root: plan.root, applied, held };
1482
1773
  }
1483
1774
  function describeStep(step, root) {
1484
- const shown = root ? relativeTo(root, step.path) : step.path.replace(userHome(), "~");
1775
+ const shown = root ? relativeTo(root, step.path) : tildify(step.path);
1485
1776
  switch (step.kind) {
1486
1777
  case "link":
1487
1778
  return `link ${shown}`;
1488
1779
  case "unlink":
1489
1780
  return `remove extra link ${shown}`;
1490
1781
  case "replace":
1491
- return `${shown}: duplicate copy \u2192 link`;
1782
+ return `${shown}: ${step.differs ? "differing" : "duplicate"} copy \u2192 link`;
1492
1783
  case "remove":
1493
- return `remove duplicate copy ${shown}`;
1784
+ return `remove ${step.differs ? "differing" : "duplicate"} copy ${shown}`;
1494
1785
  }
1495
1786
  }
1496
1787
  function copyLabel(conflict, dir) {
1497
- return conflict.root ? relativeTo(conflict.root, dir) : dir.replace(userHome(), "~");
1788
+ return folderLabel(conflict.root, dir);
1789
+ }
1790
+ function folderLabel(root, dir) {
1791
+ return root ? relativeTo(root, dir) : tildify(dir);
1792
+ }
1793
+ function foldersOf(root, name) {
1794
+ const entries = entriesOf(root ? PROJECT_TIDY_DIRS.map((d) => join12(root, d)) : globalTidyDirs(), name);
1795
+ return { all: entries.map((e) => e.dir), real: entries.filter((e) => !e.link).map((e) => e.dir) };
1498
1796
  }
1499
1797
 
1500
1798
  // src/health.ts
@@ -1543,13 +1841,14 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
1543
1841
  issues.push({
1544
1842
  id: `broken:${skill.name}`,
1545
1843
  severity: "problem",
1546
- title: `${skill.name}: broken link in ${skill.path.replace(/\/[^/]+$/, "").replace(/^\/Users\/[^/]+/, "~")}`,
1844
+ title: `${skill.name}: broken link in ${tildify(dirname5(skill.path))}`,
1547
1845
  detail: "It points at a folder that no longer exists, so it loads nothing.",
1548
1846
  fix: {
1549
1847
  label: "Remove the link",
1550
1848
  run: () => {
1551
1849
  const r = unloadGlobal(skill.path);
1552
- return r.ok ? `Removed broken link ${skill.name}` : `${skill.name}: ${r.reason}`;
1850
+ if (!r.ok) throw new Error(`${skill.name}: ${r.reason}`);
1851
+ return `Removed broken link ${skill.name}`;
1553
1852
  }
1554
1853
  }
1555
1854
  });
@@ -1570,8 +1869,10 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
1570
1869
  });
1571
1870
  }
1572
1871
  const loadedHere = new Map(projects2.map((root) => [root, loadedIn(root)]));
1573
- const seenByClaude = (s) => s.visibility.some((v) => v.id === "claude-code" && v.paths > 0);
1574
- const namesIn2 = (root) => [...new Set([...statuses.get(root), ...nested.get(root)].filter(seenByClaude).map((s) => s.name))];
1872
+ const namesIn2 = (root, agent = "claude-code") => [
1873
+ ...new Set([...statuses.get(root), ...nested.get(root)].filter((s) => s.visibility.some((v) => v.id === agent && v.paths > 0)).map((s) => s.name))
1874
+ ];
1875
+ const pluginAgentsIn = (plugin, root) => new Set(loadedHere.get(root).filter((m) => plugin.skills.includes(m.path)).flatMap((m) => m.harnesses));
1575
1876
  const globalYours = new Set(loaded.filter((m) => m.movable).map((m) => m.name));
1576
1877
  const installedYours = /* @__PURE__ */ new Set([...globalYours, ...[...statuses.values()].flat().map((s) => s.name)]);
1577
1878
  const repoChoices = (plugin, root, names) => {
@@ -1579,7 +1880,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
1579
1880
  const copies = statuses.get(root).filter((s) => names.includes(s.name));
1580
1881
  const allManaged = names.every((n) => copies.some((c) => c.name === n && c.managed));
1581
1882
  return [
1582
- { label: `Turn the plugin ${plugin.id} off in ${where} only`, hint: "other repos keep it", run: () => orThrow(turnOffIn(plugin, root)) },
1883
+ { label: `Turn the plugin ${plugin.id} off in ${where} only`, hint: "other repos keep it; in .claude/settings.local.json, a local-only file", run: () => orThrow(turnOffIn(plugin, root)) },
1583
1884
  // Only copies skilllib installed: Your skills keeps them. A repo's own skill is the team's, never removed from here.
1584
1885
  ...names.length && allManaged ? [
1585
1886
  {
@@ -1599,8 +1900,10 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
1599
1900
  const machinePlugins = claudePlugins(machine);
1600
1901
  for (const plugin of machinePlugins) {
1601
1902
  const names = plugin.skills.map((p) => basename6(p));
1602
- const inRepos = projects2.filter((root) => loadedHere.get(root).some((m) => plugin.skills.includes(m.path))).map((root) => [root, namesIn2(root).filter((n) => names.includes(n))]).filter(([, d]) => d.length);
1603
- const loadedYours = /* @__PURE__ */ new Set([...globalYours, ...inRepos.flatMap(([, d]) => d)]);
1903
+ 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);
1904
+ 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) : [];
1905
+ const claudeYours = /* @__PURE__ */ new Set([...globalYours, ...inRepos.flatMap(([, d]) => d)]);
1906
+ const loadedYours = /* @__PURE__ */ new Set([...claudeYours, ...cursorRepos.flatMap(([, d]) => d)]);
1604
1907
  const dupes = names.filter((n) => libraryNames.has(n) || loadedYours.has(n));
1605
1908
  if (!dupes.length) continue;
1606
1909
  const others = plugin.skills.length - dupes.length;
@@ -1612,15 +1915,18 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
1612
1915
  };
1613
1916
  const off = { label: `Turn the plugin ${plugin.id} off`, hint: "stays installed", run: () => orThrow(removePlugin(plugin, "off")) };
1614
1917
  const bothLoad = dupes.filter((n) => loadedYours.has(n));
1918
+ const claudeBoth = dupes.filter((n) => claudeYours.has(n));
1919
+ const cursorWheres = cursorRepos.map(([root]) => basename6(root));
1615
1920
  const notInstalled = dupes.filter((n) => !installedYours.has(n));
1616
1921
  issues.push({
1617
1922
  id: `plugin:${plugin.id}`,
1618
1923
  severity: bothLoad.length ? "problem" : "suggestion",
1619
1924
  title: `${dupes.join(", ")}: also in the Claude Code plugin ${plugin.id}`,
1620
1925
  detail: [
1621
- bothLoad.length ? `Claude Code loads both (the plugin's as /${pluginName}:${bothLoad[0]}). Your skill wins.` : "",
1926
+ claudeBoth.length ? `Claude Code loads both (the plugin's as /${pluginName}:${claudeBoth[0]}). Your skill wins.` : "",
1927
+ cursorWheres.length ? `${cursorWheres.join(", ")} ${cursorWheres.length === 1 ? "turns" : "turn"} it off for Claude Code, but Cursor ignores repo settings and lists both there: only turning the plugin off or removing it changes that.` : "",
1622
1928
  notInstalled.length ? `Your ${notInstalled.join(", ")} ${notInstalled.length === 1 ? "is" : "are"} only in Your skills, not installed anywhere: once the plugin is gone, add ${notInstalled.length === 1 ? "it" : "them"} where you need ${notInstalled.length === 1 ? "it" : "them"}.` : "",
1623
- others ? `The plugin's other ${plural(others, "skill")} are copied into Your skills first, so nothing is lost.` : "",
1929
+ others ? `The plugin's other ${others === 1 ? "skill is" : `${others} skills are`} copied into Your skills first, so nothing is lost.` : "",
1624
1930
  plugin.extras.length ? `It also brings ${plugin.extras.join(", ")}: ${plugin.synced ? "turning it off pauses those" : "removing it drops those, turning it off pauses them"}.` : "",
1625
1931
  plugin.synced ? "It's synced from claude.ai, so it can only be turned off here." : "",
1626
1932
  inRepos.length ? `Or keep it and turn it off only in ${inRepos.map(([root]) => basename6(root)).join(", ")}.` : ""
@@ -1667,7 +1973,11 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
1667
1973
  detail: `${global.plans.map((p) => p.name).join(", ")}: each keeps one copy (in ~/.agents/skills when there's one) and the others become links. Restorable from Health.`,
1668
1974
  fix: {
1669
1975
  label: "Keep one copy of each",
1670
- run: () => `${plural(applyAll(global.plans, "go").skills, "global skill")} now have one copy`
1976
+ run: () => {
1977
+ const r = applyAll(global.plans, "go");
1978
+ if (!r.skills) throw new Error("No global skill changed");
1979
+ return `${plural(r.skills, "global skill")} now ${r.skills === 1 ? "has" : "have"} one copy`;
1980
+ }
1671
1981
  }
1672
1982
  });
1673
1983
  }
@@ -1679,7 +1989,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
1679
1989
  id: "review:global",
1680
1990
  severity: "suggestion",
1681
1991
  title: `${plural(unreviewed.length, "global skill")} of yours ${unreviewed.length === 1 ? "loads" : "load"} in every repo`,
1682
- detail: `${unreviewed.join(", ")}. Choose which repos keep each (Global \u2192 Clean up\u2026), or mark the ones you want everywhere as global on purpose (skilllib global keep <name>).`
1992
+ detail: `${unreviewed.join(", ")}. Move each to the repos that need it (skilllib \u2192 Global \u2192 Move it to the repos that need it\u2026), or mark the ones you want everywhere as global on purpose (skilllib global keep <name>).`
1683
1993
  });
1684
1994
  }
1685
1995
  const enabled = enabledHarnesses();
@@ -1697,6 +2007,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
1697
2007
  run: () => {
1698
2008
  const results = skills.map((m) => ({ m, ...linkGlobal(m.path, blindOf(m)) }));
1699
2009
  const skipped = results.flatMap((r) => r.skipped);
2010
+ if (!results.some((r) => r.linked.length)) throw new Error(`Nothing linked${skipped.length ? `; skipped ${skipped.join(", ")} (a different skill has that name)` : ""}`);
1700
2011
  return `Linked ${plural(results.filter((r) => r.linked.length).length, "global skill")} for ${agentNames(missing)}${skipped.length ? `; skipped ${skipped.join(", ")} (a different skill has that name)` : ""}`;
1701
2012
  }
1702
2013
  };
@@ -1714,28 +2025,69 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
1714
2025
  for (const root of projects2) {
1715
2026
  const where = basename6(root);
1716
2027
  const status2 = statuses.get(root);
1717
- const behind = status2.filter((s) => s.state === "update available" || s.state === "folder missing");
1718
- if (behind.length > 0) {
2028
+ const restorable = status2.filter((s) => s.state === "folder missing" && s.latest !== null);
2029
+ if (restorable.length > 0) {
1719
2030
  issues.push({
1720
2031
  id: `sync:${root}`,
1721
2032
  severity: "problem",
1722
- title: `${where}: ${plural(behind.length, "skill")} out of date`,
1723
- detail: behind.map((s) => s.name).join(", "),
2033
+ title: `${where}: ${plural(restorable.length, "skill")} in skilllib.json not installed`,
2034
+ detail: `${restorable.map((s) => s.name).join(", ")}: the folder is missing`,
1724
2035
  fix: {
1725
2036
  label: "Sync the project",
1726
2037
  run: () => {
1727
2038
  const changes = syncProject(root);
1728
- return `${where}: ${changes.filter((c) => c.action !== "skipped").length} updated`;
2039
+ const done = changes.filter((c) => c.action !== "skipped");
2040
+ const skipped = changes.filter((c) => c.action === "skipped").map((c) => `${c.name}: ${c.reason}`);
2041
+ if (!done.length) throw new Error(`${where}: nothing restored${skipped.length ? `; ${skipped.join("; ")}` : ""}`);
2042
+ return `${where}: restored ${plural(done.length, "skill")}${skipped.length ? `; skipped ${skipped.join("; ")}` : ""}`;
1729
2043
  }
1730
2044
  }
1731
2045
  });
1732
2046
  }
2047
+ for (const s of status2.filter((s2) => s2.state === "folder missing" && s2.latest === null))
2048
+ issues.push({
2049
+ id: `unavailable:${root}:${s.name}`,
2050
+ severity: "problem",
2051
+ title: `${s.name}: pinned in ${where}'s skilllib.json, but not in your library`,
2052
+ 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.`,
2053
+ // Editing skilllib.json is the team's call: a choice, never run by `doctor --fix`.
2054
+ choices: [
2055
+ {
2056
+ label: "Remove it from skilllib.json",
2057
+ hint: `${where} stops pinning it`,
2058
+ run: () => {
2059
+ const r = removeSkill(root, s.name);
2060
+ if (r.action !== "removed") throw new Error(`${s.name}: ${r.reason}`);
2061
+ return `${s.name}: removed from ${where}'s skilllib.json`;
2062
+ }
2063
+ }
2064
+ ]
2065
+ });
2066
+ const behind = status2.filter((s) => s.state === "update available");
2067
+ if (behind.length > 0)
2068
+ issues.push({
2069
+ id: `update:${root}`,
2070
+ severity: "suggestion",
2071
+ title: `${where}: ${plural(behind.length, "skill")} out of date`,
2072
+ detail: behind.map((s) => `${s.name} (v${s.version} \u2192 v${s.latest})`).join(", "),
2073
+ fix: {
2074
+ label: "Update to the newest versions",
2075
+ run: () => {
2076
+ const changes = updateProject(root, behind.map((s) => s.name));
2077
+ const done = changes.filter((c) => c.action !== "skipped");
2078
+ const skipped = changes.filter((c) => c.action === "skipped").map((c) => `${c.name}: ${c.reason}`);
2079
+ if (!done.length) throw new Error(`${where}: nothing updated${skipped.length ? `; ${skipped.join("; ")}` : ""}`);
2080
+ return `${where}: updated ${plural(done.length, "skill")}${skipped.length ? `; skipped ${skipped.join("; ")}` : ""}`;
2081
+ }
2082
+ }
2083
+ });
1733
2084
  const tidy2 = planProjectTidy(root);
1734
2085
  if (tidy2.plans.length) {
1735
- const git2 = gitInfo(root);
1736
- const visible2 = tidy2.plans.flatMap((p) => gitVisibleSteps(p, git2).map((s) => describeStep(s, root)));
2086
+ const git3 = gitInfo(root);
2087
+ const visible2 = tidy2.plans.flatMap((p) => gitVisibleSteps(p, git3).map((s) => describeStep(s, root)));
1737
2088
  const run = (mode) => () => {
1738
2089
  const r = applyAll(tidy2.plans, mode);
2090
+ if (!r.skills) throw new Error(`${where}: nothing tidied${r.held ? `; left ${plural(r.held, "change")} git would see` : ""}`);
1739
2091
  return `${where}: tidied ${plural(r.skills, "skill")}${r.held ? `; left ${plural(r.held, "change")} git would see` : ""}`;
1740
2092
  };
1741
2093
  issues.push({
@@ -1752,14 +2104,24 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
1752
2104
  });
1753
2105
  }
1754
2106
  issues.push(...tidy2.conflicts.map(conflictIssue));
1755
- const partial = status2.filter((s) => s.state !== "folder missing" && s.visibility.some((v) => v.paths === 0));
2107
+ 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 }) }));
2108
+ for (const { s, would } of blindHere.filter(({ would: w }) => !w.created.length && !w.blocked.length && w.uncommitted.length))
2109
+ issues.push({
2110
+ id: `uncommitted:${root}:${s.name}`,
2111
+ severity: "suggestion",
2112
+ 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`,
2113
+ 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).`
2114
+ });
2115
+ const partial = blindHere.filter(({ would }) => would.created.length || would.blocked.length).map(({ s }) => s);
1756
2116
  if (partial.length) {
1757
2117
  const missing = [...new Set(partial.flatMap((s) => s.visibility.filter((v) => v.paths === 0).map((v) => v.id)))];
1758
- const tracked = readConfig().agentsDirOk?.includes(root) ? [] : [...new Set(missing.map((id) => harness(id).projectDirs[0]))].filter((dir) => isGitTracked(root, dir));
2118
+ const tracked = [...new Set(blindHere.flatMap(({ would }) => would.blocked))];
1759
2119
  const run = (allowTracked) => () => {
1760
2120
  if (allowTracked) allowTrackedLinks(root);
1761
2121
  const r = linkAll(root, { allowTracked });
1762
- return `${where}: linked ${plural(r.linked.length, "skill")} for ${agentNames(missing)}${r.blocked.length ? `; skipped ${r.blocked.join(", ")} (git tracks it)` : ""}`;
2122
+ 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)` : ""}`;
2123
+ if (!r.linked.length) throw new Error(`${where}: nothing linked${notes}`);
2124
+ return `${where}: linked ${plural(r.linked.length, "skill")} for ${agentNames(missing)}${notes}`;
1763
2125
  };
1764
2126
  issues.push({
1765
2127
  id: `usable:${root}`,
@@ -1768,20 +2130,40 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
1768
2130
  detail: `${partial.map((s) => `${s.name} (not ${agentNames(s.visibility.filter((v) => v.paths === 0).map((v) => v.id))})`).join(", ")}. Adding links makes the one real copy reach every agent you use; nothing is copied or moved.${tracked.length ? ` Git tracks ${tracked.join(", ")}, so links there show in git status.` : ""}`,
1769
2131
  ...tracked.length ? {
1770
2132
  choices: [
1771
- { label: "Add links, but not where git tracks the folder", hint: `skips ${tracked.join(", ")}`, run: run(false) },
2133
+ // Only when some link goes where git doesn't track: otherwise this one would link nothing.
2134
+ ...blindHere.some(({ would }) => would.created.length) ? [{ label: "Add links, but not where git tracks the folder", hint: `skips ${tracked.join(", ")}`, run: run(false) }] : [],
1772
2135
  { label: "Add links everywhere", hint: `remembered for ${where}; git will see them`, run: run(true) }
1773
2136
  ]
1774
2137
  } : { fix: { label: "Add the links", run: run(false) } }
1775
2138
  });
1776
2139
  }
1777
2140
  const differing = new Set(tidy2.conflicts.map((c) => c.name));
2141
+ const git2 = gitInfo(root);
2142
+ const committed = (s) => ["committed", "changed"].includes(git2?.of(relativeTo(root, s.path)) ?? "");
1778
2143
  for (const s of status2) {
1779
2144
  if (isLink(s.path) || differing.has(s.name)) continue;
1780
- if (s.state === "local only") {
2145
+ if (s.state === "local only" && committed(s)) {
1781
2146
  issues.push({
1782
2147
  id: `local:${root}:${s.name}`,
1783
2148
  severity: "suggestion",
1784
- title: `${s.name}: only exists in ${where}`,
2149
+ title: `${s.name} in ${where} isn't in your library`,
2150
+ 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).",
2151
+ choices: [
2152
+ {
2153
+ label: "Copy into the library",
2154
+ hint: `${where}'s copy stays as it is`,
2155
+ run: () => {
2156
+ importSkill(s.path);
2157
+ return `${s.name} copied into your library; ${where}'s copy stays as it is`;
2158
+ }
2159
+ }
2160
+ ]
2161
+ });
2162
+ } else if (s.state === "local only") {
2163
+ issues.push({
2164
+ id: `local:${root}:${s.name}`,
2165
+ severity: "suggestion",
2166
+ title: `${s.name} in ${where} isn't in your library`,
1785
2167
  detail: "Import it into the library to reuse it in other projects and keep it backed up. The repo then tracks it in skilllib.json.",
1786
2168
  choices: [
1787
2169
  {
@@ -1805,17 +2187,17 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
1805
2187
  });
1806
2188
  }
1807
2189
  if (s.state === "edited locally" || s.state === "edited locally, update available") {
1808
- const newer = s.state !== "edited locally";
2190
+ const newer2 = s.state !== "edited locally";
1809
2191
  const pinned = s.version ? `v${s.version}` : "the library version";
1810
2192
  issues.push({
1811
2193
  id: `edited:${root}:${s.name}`,
1812
2194
  severity: "suggestion",
1813
2195
  title: `${s.name} in ${where} has local edits`,
1814
- detail: `It no longer matches ${pinned} from Your skills${newer ? `, and v${s.latest} is out` : ""}. Save the edits as a new version so your other repos can get them, or discard them. Keeping them is fine too: skilllib never overwrites edits.`,
2196
+ detail: `It no longer matches ${pinned} from Your skills${newer2 ? `, and v${s.latest} is out` : ""}. Save the edits as a new version so your other repos can get them, or discard them. Keeping them is fine too: skilllib never overwrites edits.`,
1815
2197
  choices: [
1816
2198
  {
1817
2199
  label: "Save as a new version in Your skills",
1818
- hint: newer ? `becomes v${s.latest + 1}; v${s.latest}'s changes aren't merged in` : `becomes v${(s.latest ?? 0) + 1}; ${where} uses it`,
2200
+ hint: newer2 ? `becomes v${s.latest + 1}; v${s.latest}'s changes aren't merged in` : `becomes v${(s.latest ?? 0) + 1}; ${where} uses it`,
1819
2201
  run: () => {
1820
2202
  importSkill(s.path, { force: true });
1821
2203
  const c = addSkill(root, s.name);
@@ -1984,7 +2366,7 @@ function groupBy(items) {
1984
2366
  }
1985
2367
  var sameSet = (a, b) => a.length === b.length && a.every((x) => b.includes(x));
1986
2368
  function globalFrom(kind, origin, path) {
1987
- if (kind === "global") return tildify(path.replace(/[\\/][^\\/]+$/, "")).replace(/\\/g, "/");
2369
+ if (kind === "global" || kind === "skilllib") return tildify(path.replace(/[\\/][^\\/]+$/, "")).replace(/\\/g, "/");
1988
2370
  return origin;
1989
2371
  }
1990
2372
  var SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "target", "vendor", "coverage"]);
@@ -1997,28 +2379,34 @@ function topLevel(root) {
1997
2379
  }
1998
2380
  function usableHere(root, uses = /* @__PURE__ */ new Map()) {
1999
2381
  const agents = enabledHarnesses();
2000
- const git2 = gitInfo(root);
2001
- const inRepo = isProjectCandidate(root) && (existsSync12(join13(root, ".git")) || existsSync12(join13(root, MANIFEST_FILE)));
2002
- const project = inRepo ? projectStatus(root) : [];
2003
- const skills = groupBy(
2004
- project.map((s) => {
2382
+ const inRepo = projectHere(root) === root;
2383
+ const nestedHere = inRepo ? nestedSkills(root, { enabled: agents }) : [];
2384
+ const git2 = gitInfo(root, [...new Set(nestedHere.map((s) => s.location))]);
2385
+ const group = (list2) => groupBy(
2386
+ list2.map((s) => {
2005
2387
  const loadedBy = s.visibility.filter((v) => v.paths > 0).map((v) => v.id);
2006
2388
  const behind = s.managed && s.latest !== null && s.version !== null && s.latest > s.version;
2389
+ const gitState = git2?.of(relativeTo(root, s.path));
2390
+ const repo = !s.managed && (s.state.startsWith("repo skill") || gitState === "committed" || gitState === "changed");
2391
+ const usual = s.state === "ok" || s.state === "local only" || repo && s.state !== "from npx skills";
2007
2392
  const attrs = {
2008
- source: s.managed ? "library" : s.state.startsWith("repo skill") ? "repo" : "local",
2009
- ...s.location !== ".claude/skills" && { dir: s.location },
2010
- ...s.version !== null && { version: s.version },
2011
- ...s.state !== "ok" && s.state !== "local only" && !s.state.startsWith("repo skill") && { state: s.state },
2012
- ...behind && { latest: s.latest },
2013
- ...git2 && { git: git2.of(relativeTo(root, s.path)) },
2014
- ...!sameSet(loadedBy, agents) && { agents: loadedBy }
2393
+ source: s.managed ? "library" : repo ? "repo" : "local",
2394
+ dir: s.location !== ".claude/skills" ? s.location : void 0,
2395
+ version: s.version ?? void 0,
2396
+ state: usual ? void 0 : s.state,
2397
+ latest: behind ? s.latest : void 0,
2398
+ git: gitState,
2399
+ agents: sameSet(loadedBy, agents) ? void 0 : loadedBy
2015
2400
  };
2016
2401
  return [JSON.stringify(attrs), s.name, () => ({ ...attrs, skills: [] })];
2017
2402
  })
2018
2403
  );
2404
+ const skills = group(inRepo ? projectStatus(root) : []);
2405
+ const nested = group(nestedHere);
2019
2406
  const machine = machineSkills(agents);
2407
+ const loaded = inRepo ? skillsLoadedIn(root, machine, agents) : machine;
2020
2408
  const global = groupBy(
2021
- machine.filter((m) => !m.broken && m.harnesses.length > 0).map((m) => {
2409
+ loaded.filter((m) => !m.broken && m.harnesses.length > 0).map((m) => {
2022
2410
  const from = globalFrom(m.kind, m.origin, m.path);
2023
2411
  return [`${m.kind}\0${from}\0${m.harnesses.join()}`, m.name, () => ({ source: m.kind, from, agents: m.harnesses, skills: [] })];
2024
2412
  })
@@ -2027,7 +2415,7 @@ function usableHere(root, uses = /* @__PURE__ */ new Map()) {
2027
2415
  for (const i of findIssues(inRepo ? [root] : [], machine, new Set(librarySkills().map((l) => l.name)))) {
2028
2416
  const choices = i.choices?.map((c) => c.label);
2029
2417
  const key = `${i.detail}\0${i.fix?.label ?? ""}\0${choices?.join() ?? ""}`;
2030
- const issue = issues.get(key) ?? { problems: [], detail: i.detail, ...i.fix && { fix: i.fix.label }, ...choices && { choices } };
2418
+ const issue = issues.get(key) ?? { problems: [], detail: i.detail, fix: i.fix?.label, choices };
2031
2419
  issue.problems.push(i.title);
2032
2420
  issues.set(key, issue);
2033
2421
  }
@@ -2037,9 +2425,10 @@ function usableHere(root, uses = /* @__PURE__ */ new Map()) {
2037
2425
  files: topLevel(root),
2038
2426
  agents,
2039
2427
  skills,
2428
+ nested: nested.length > 0 ? nested : void 0,
2040
2429
  global,
2041
2430
  issues: [...issues.values()],
2042
- ...Object.keys(usesHere).length > 0 && { usesHere }
2431
+ usesHere: Object.keys(usesHere).length > 0 ? usesHere : void 0
2043
2432
  };
2044
2433
  }
2045
2434
  function firstSentence(description) {
@@ -2047,21 +2436,21 @@ function firstSentence(description) {
2047
2436
  const sentence = line.match(/^(.{40,}?[.!?])(?:\s|$)/)?.[1] ?? line;
2048
2437
  return sentence.length > 200 ? sentence.slice(0, 199).trimEnd() + "\u2026" : sentence;
2049
2438
  }
2050
- function libraryFor(currentProject2) {
2439
+ function libraryFor(currentProject) {
2051
2440
  const manifests = visibleProjects().map((p) => ({ name: basename7(p), skills: readManifest(p).skills }));
2052
- const here = currentProject2 ? new Set(projectStatus(currentProject2).map((s) => s.name)) : null;
2441
+ const here = currentProject ? new Set(projectStatus(currentProject).map((s) => s.name)) : null;
2053
2442
  const all = librarySkills();
2054
- const describe = (s) => {
2443
+ const describe2 = (s) => {
2055
2444
  const projects2 = manifests.filter((m) => s.name in m.skills).map((m) => m.name);
2056
- return { name: s.name, description: firstSentence(s.description), ...projects2.length > 0 && { projects: projects2 } };
2445
+ return { name: s.name, description: firstSentence(s.description), projects: projects2.length > 0 ? projects2 : void 0 };
2057
2446
  };
2058
- if (!here) return { skills: all.map(describe) };
2059
- return { inThisProject: all.filter((s) => here.has(s.name)).map((s) => s.name), skills: all.filter((s) => !here.has(s.name)).map(describe) };
2447
+ if (!here) return { skills: all.map(describe2) };
2448
+ return { inThisProject: all.filter((s) => here.has(s.name)).map((s) => s.name), skills: all.filter((s) => !here.has(s.name)).map(describe2) };
2060
2449
  }
2061
2450
 
2062
2451
  // src/usage.ts
2063
- import { createReadStream, existsSync as existsSync13, readdirSync as readdirSync8, statSync as statSync3 } from "fs";
2064
- import { join as join14 } from "path";
2452
+ import { createReadStream, existsSync as existsSync12, readdirSync as readdirSync8, statSync as statSync4 } from "fs";
2453
+ import { join as join13 } from "path";
2065
2454
  import { createInterface } from "readline";
2066
2455
  var BUILTIN_COMMANDS = /* @__PURE__ */ new Set([
2067
2456
  "add-dir",
@@ -2141,12 +2530,12 @@ function usesFromTranscriptLine(line) {
2141
2530
  return [];
2142
2531
  }
2143
2532
  function transcriptFiles(sinceMs) {
2144
- const base = join14(claudeDir(), "projects");
2145
- if (!existsSync13(base)) return [];
2533
+ const base = join13(claudeDir(), "projects");
2534
+ if (!existsSync12(base)) return [];
2146
2535
  return readdirSync8(base).flatMap((project) => {
2147
- const dir = join14(base, project);
2148
- if (!statSync3(dir).isDirectory()) return [];
2149
- return readdirSync8(dir).filter((f) => f.endsWith(".jsonl")).map((f) => join14(dir, f)).filter((f) => statSync3(f).mtimeMs >= sinceMs);
2536
+ const dir = join13(base, project);
2537
+ if (!statSync4(dir).isDirectory()) return [];
2538
+ return readdirSync8(dir).filter((f) => f.endsWith(".jsonl")).map((f) => join13(dir, f)).filter((f) => statSync4(f).mtimeMs >= sinceMs);
2150
2539
  });
2151
2540
  }
2152
2541
  async function scanUsage(days) {
@@ -2210,29 +2599,40 @@ function parseArgs(argv) {
2210
2599
  function colorState(state) {
2211
2600
  if (state === "ok") return green(state);
2212
2601
  if (state === "folder missing" || state === "not in library") return red(state);
2213
- if (state === "local only" || state === "from npx skills") return dim(state);
2602
+ if (state === "local only" || state === "from npx skills" || state === "repo skill") return dim(state);
2214
2603
  return yellow(state);
2215
2604
  }
2216
2605
  function withDirs(root, changes) {
2217
2606
  const { skills } = readManifest(root);
2218
2607
  return changes.map((c) => {
2219
- const dep = c.action === "installed" || c.action === "updated" ? skills[c.name] : void 0;
2608
+ const dep = c.action === "installed" || c.action === "updated" || c.action === "reset" ? skills[c.name] : void 0;
2220
2609
  return dep ? { ...c, dirs: [dep.dir ?? ".claude/skills", ...dep.links ?? []] } : c;
2221
2610
  });
2222
2611
  }
2223
2612
  function printChanges(root, changes, args) {
2224
- if (args.json) return json(withDirs(root, changes));
2613
+ if (changes.some((c) => c.action === "skipped")) process.exitCode = 1;
2614
+ if (args.json) return json("changes", withDirs(root, changes));
2225
2615
  for (const c of changes) {
2226
2616
  if (c.blocked?.length) {
2227
2617
  warn(`${c.name}: git tracks ${c.blocked.join(", ")} here, so no link was added there. Re-run with --allow-tracked to add it.`);
2228
2618
  }
2229
- if (c.action === "skipped") warn(`${c.name}: ${c.reason}`);
2619
+ if (c.uncommitted?.length) {
2620
+ warn(
2621
+ `${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.`
2622
+ );
2623
+ }
2624
+ if (c.action === "skipped") error(`${c.name}: ${c.reason}`);
2230
2625
  else {
2231
- const icon = c.action === "removed" ? red("-") : green(c.action === "installed" ? "+" : "\u2191");
2626
+ const icon = c.action === "removed" ? red("-") : c.action === "reset" ? yellow("\u21BA") : green(c.action === "installed" ? "+" : "\u2191");
2232
2627
  const versions = c.action === "removed" ? "" : c.from && c.from !== c.to ? ` v${c.from} \u2192 v${c.to}` : c.to ? ` v${c.to}` : "";
2233
2628
  info(` ${icon} ${c.name}${versions} ${dim(c.action)}`);
2234
2629
  }
2630
+ if (c.backedUp) info(dim(` local edits saved to ${tildify(c.backedUp)}; \`skilllib restore ${c.name}\` brings them back`));
2235
2631
  }
2632
+ const done = changes.filter((c) => c.action !== "skipped").length;
2633
+ const skipped = changes.length - done;
2634
+ if (skipped && done) info(dim(`
2635
+ ${done} done, ${skipped} skipped`));
2236
2636
  }
2237
2637
  function projectOfFactory(projects2) {
2238
2638
  const sorted = [...projects2].sort((a, b) => b.length - a.length);
@@ -2250,35 +2650,51 @@ async function usageBySkill(days, onlyProject) {
2250
2650
  const scoped = onlyProject ? uses.filter((u) => projectOf(u.cwd) === onlyProject) : uses;
2251
2651
  return new Map(summarize(scoped, projectOf).map((s) => [s.skill, s]));
2252
2652
  }
2253
- function currentProject() {
2254
- const root = findProjectRoot();
2255
- return isProjectCandidate(root) && (existsSync14(join15(root, ".git")) || existsSync14(join15(root, "skilllib.json"))) ? root : null;
2653
+ function requireProject() {
2654
+ const root = projectHere();
2655
+ if (root) return root;
2656
+ if (!isProjectCandidate(findProjectRoot())) {
2657
+ const stray = existsSync13(join14(userHome(), MANIFEST_FILE)) ? `, though ${tildify(join14(userHome(), MANIFEST_FILE))} makes it look like one` : "";
2658
+ throw new Error(`Your home folder isn't a project${stray}: skills added here would load in every repo. Run this inside a project.`);
2659
+ }
2660
+ throw new Error("Not in a git repo. Run this inside a project (or `git init` first).");
2256
2661
  }
2257
2662
  function lastUsed(summary) {
2258
2663
  return summary ? summary.lastUsed.slice(0, 10) : "-";
2259
2664
  }
2260
2665
  async function status(args) {
2261
- const root = findProjectRoot();
2262
2666
  if (args.json) {
2263
- const usage3 = await usageBySkill(args.days, root);
2264
- json(usableHere(root, new Map([...usage3].map(([name, u]) => [name, u.uses]))));
2265
- if (currentProject()) rememberProjects([root]);
2667
+ const root2 = findProjectRoot();
2668
+ const usage3 = await usageBySkill(args.days, root2);
2669
+ json("status", usableHere(root2, new Map([...usage3].map(([name, u]) => [name, u.uses]))));
2670
+ if (projectHere()) rememberProjects([root2]);
2266
2671
  return;
2267
2672
  }
2673
+ const root = requireProject();
2268
2674
  const skills = projectStatus(root);
2269
2675
  const nested = nestedSkills(root);
2270
2676
  info(`${basename8(root)} ${dim(tildify(root))}
2271
2677
  `);
2272
- if (skills.length === 0 && nested.length === 0) {
2273
- info("No skills in this project yet. Add one from your library with: skilllib add <name>");
2274
- return;
2275
- }
2678
+ if (skills.length === 0 && nested.length === 0) info("No skills in this project yet. Add one from your library with: skilllib add <name>");
2276
2679
  const usage2 = await usageBySkill(args.days, root);
2680
+ const git2 = gitInfo(root, [...new Set(nested.map((s) => s.location))]);
2681
+ const committed = (path) => ["committed", "changed"].includes(git2?.of(relativeTo(root, path)) ?? "");
2682
+ const asRepo = {
2683
+ "local only": "repo skill",
2684
+ "untracked copy of library skill": "repo skill, in library",
2685
+ "untracked, differs from library": "repo skill, differs from library"
2686
+ };
2687
+ const stateOf = (s) => colorState(committed(s.path) && asRepo[s.state] || s.state);
2688
+ const agentsOf = (s) => {
2689
+ const ids = s.visibility.filter((v) => v.paths > 0).map((v) => v.id);
2690
+ return ids.length === s.visibility.length ? dim("all") : ids.length ? ids.map((id) => harness(id).name).join(", ") : red("none");
2691
+ };
2277
2692
  if (skills.length) {
2278
2693
  table(
2279
2694
  skills.map((s) => ({
2280
2695
  Skill: s.name,
2281
- Status: s.source ? dim(`npx skills: ${s.source}`) : colorState(s.state),
2696
+ Status: s.source ? dim(`npx skills: ${s.source}`) : stateOf(s),
2697
+ Agents: agentsOf(s),
2282
2698
  [`Uses (${args.days}d)`]: String(usage2.get(s.name)?.uses ?? 0),
2283
2699
  "Last used": lastUsed(usage2.get(s.name))
2284
2700
  }))
@@ -2290,7 +2706,8 @@ async function status(args) {
2290
2706
  nested.map((s) => ({
2291
2707
  Skill: s.name,
2292
2708
  Folder: s.location,
2293
- Status: colorState(s.state),
2709
+ Status: stateOf(s),
2710
+ Agents: agentsOf(s),
2294
2711
  [`Uses (${args.days}d)`]: String(usage2.get(s.name)?.uses ?? 0)
2295
2712
  }))
2296
2713
  );
@@ -2298,18 +2715,32 @@ async function status(args) {
2298
2715
  const states = new Set(skills.map((s) => s.state));
2299
2716
  const hints = [];
2300
2717
  if (states.has("update available")) hints.push("`skilllib update` to install the newest versions");
2301
- if (states.has("folder missing")) hints.push("`skilllib sync` to restore missing skills");
2718
+ const missing = skills.filter((s) => s.state === "folder missing");
2719
+ if (missing.some((s) => s.latest !== null)) hints.push("`skilllib sync` to restore missing skills");
2720
+ const notInLibrary = missing.filter((s) => s.latest === null).map((s) => s.name);
2721
+ if (notInLibrary.length)
2722
+ hints.push(`${notInLibrary.join(", ")} ${notInLibrary.length === 1 ? "isn't" : "aren't"} in your library (${tildify(libraryDir())}): \`skilllib import <dir>\` to add, or set SKILLLIB_HOME to the library that has ${notInLibrary.length === 1 ? "it" : "them"}`);
2302
2723
  if (states.has("edited locally") || states.has("edited locally, update available"))
2303
2724
  hints.push("`skilllib import .claude/skills/<name> --force` to save local edits to the library");
2304
2725
  if (states.has("local only")) hints.push("`skilllib import .claude/skills/<name>` to add a local skill to the library");
2305
2726
  if (states.has("untracked copy of library skill") || states.has("untracked, differs from library"))
2306
2727
  hints.push("`skilllib add <name>` to manage an untracked skill (--force if it differs)");
2307
2728
  if (hints.length > 0) info("\n" + hints.map((h) => dim(`\u2192 ${h}`)).join("\n"));
2729
+ const here = usableHere(root);
2730
+ const globals = new Set(here.global.flatMap((g) => g.skills)).size;
2731
+ if (globals) info(`
2732
+ ${globals} global ${globals === 1 ? "skill also loads" : "skills also load"} here ${dim("(your global folders, plugins, claude.ai): `skilllib` lists them")}`);
2733
+ const problems = here.issues.reduce((n, i) => n + i.problems.length, 0);
2734
+ if (problems) warn(`${problems} ${problems === 1 ? "issue" : "issues"} here or in your global skills: \`skilllib doctor\` lists them with fixes`);
2308
2735
  rememberProjects([root]);
2309
2736
  }
2737
+ async function overview(args) {
2738
+ if (args.json || projectHere()) return status(args);
2739
+ info("Not in a project. `skilllib projects` lists yours, `skilllib list` your library, `skilllib global` your global skills.");
2740
+ }
2310
2741
  async function list(args) {
2311
2742
  if (args.json) {
2312
- return json(libraryFor(currentProject()));
2743
+ return json("list", libraryFor(projectHere()));
2313
2744
  }
2314
2745
  const skills = librarySkills();
2315
2746
  if (skills.length === 0) {
@@ -2337,14 +2768,14 @@ async function list(args) {
2337
2768
  );
2338
2769
  }
2339
2770
  const libraryNames = new Set(skills.map((s) => s.name));
2340
- const globals = machineSkills().filter((m) => m.movable && !m.broken);
2771
+ const globals = [...new Set(machineSkills().filter((m) => m.movable && !m.broken).map((m) => m.name))];
2341
2772
  if (globals.length > 0) {
2342
2773
  info("");
2343
- warn(`${globals.length} of your global skill(s) still load in every project`);
2344
- const missing = globals.filter((g) => !libraryNames.has(g.name));
2774
+ warn(`${globals.length} of your global ${globals.length === 1 ? "skill still loads" : "skills still load"} in every project`);
2775
+ const missing = globals.filter((g) => !libraryNames.has(g));
2345
2776
  info(
2346
2777
  dim(
2347
- missing.length > 0 ? `\u2192 \`skilllib import --global\` copies ${missing.length} of them into the library` : "\u2192 all are in your library; run `skilllib` \u2192 Global skills to stop loading them everywhere"
2778
+ missing.length > 0 ? `\u2192 \`skilllib import --global\` copies ${missing.length} of them into the library` : "\u2192 all are in your library; run `skilllib` \u2192 Global to stop loading them everywhere"
2348
2779
  )
2349
2780
  );
2350
2781
  }
@@ -2353,11 +2784,16 @@ function projects(args) {
2353
2784
  const roots = visibleProjects();
2354
2785
  if (args.json) {
2355
2786
  return json(
2356
- roots.map((root) => ({
2357
- name: basename8(root),
2358
- path: root,
2359
- skills: projectStatus(root).map((s) => ({ name: s.name, managed: s.managed, state: s.state, version: s.version, latest: s.latest }))
2360
- }))
2787
+ "projects",
2788
+ roots.map(
2789
+ (root) => ({
2790
+ name: basename8(root),
2791
+ path: root,
2792
+ skills: projectStatus(root).map(
2793
+ (s) => ({ name: s.name, managed: s.managed, state: s.state, version: s.version, latest: s.latest })
2794
+ )
2795
+ })
2796
+ )
2361
2797
  );
2362
2798
  }
2363
2799
  if (roots.length === 0) {
@@ -2384,7 +2820,7 @@ function add(args) {
2384
2820
  error("Usage: skilllib add <skill-name>... [--force]");
2385
2821
  process.exit(1);
2386
2822
  }
2387
- const root = findProjectRoot();
2823
+ const root = requireProject();
2388
2824
  if (args.allowTracked) allowTrackedLinks(root);
2389
2825
  printChanges(root, args.positional.map((name) => addSkill(root, name, { force: args.force })), args);
2390
2826
  rememberProjects([root]);
@@ -2394,29 +2830,38 @@ function remove(args) {
2394
2830
  error("Usage: skilllib remove <skill-name>... [--force]");
2395
2831
  process.exit(1);
2396
2832
  }
2397
- const root = findProjectRoot();
2833
+ const root = requireProject();
2398
2834
  printChanges(root, args.positional.map((name) => removeSkill(root, name, { force: args.force })), args);
2399
2835
  }
2400
2836
  function sync(args) {
2401
- const roots = args.all ? visibleProjects() : [findProjectRoot()];
2837
+ const roots = args.all ? visibleProjects() : [requireProject()];
2402
2838
  const results = roots.map((root) => {
2403
2839
  if (args.allowTracked) allowTrackedLinks(root);
2404
2840
  const changes = syncProject(root, { force: args.force });
2841
+ if (changes.some((c) => c.action === "skipped")) process.exitCode = 1;
2405
2842
  if (!args.json) {
2406
2843
  if (args.all) info(`${basename8(root)} ${dim(tildify(root))}`);
2407
- if (changes.length === 0) success("Installed versions match skilllib.json");
2408
- else printChanges(root, changes, args);
2844
+ if (changes.length === 0) {
2845
+ if (existsSync13(join14(root, MANIFEST_FILE))) success("Installed versions match skilllib.json");
2846
+ else info(dim("No skilllib.json: nothing to sync"));
2847
+ } else printChanges(root, changes, args);
2409
2848
  }
2410
2849
  return { project: basename8(root), path: root, changes: withDirs(root, changes) };
2411
2850
  });
2412
- if (args.json) json(args.all ? results : results[0].changes);
2851
+ if (args.json) args.all ? json("changes --all", results) : json("changes", results[0].changes);
2413
2852
  if (!args.all) rememberProjects(roots);
2414
2853
  }
2415
2854
  function update(args) {
2416
- const roots = args.all ? visibleProjects() : [findProjectRoot()];
2855
+ const roots = args.all ? visibleProjects() : [requireProject()];
2417
2856
  const results = roots.map((root) => {
2418
2857
  if (args.allowTracked) allowTrackedLinks(root);
2419
- const changes = updateProject(root, args.positional.length ? args.positional : void 0, { force: args.force });
2858
+ const names = args.positional.length ? args.positional : void 0;
2859
+ const changes = updateProject(root, names, { force: args.force });
2860
+ if (!args.all) {
2861
+ const { skills } = readManifest(root);
2862
+ for (const name of names ?? []) if (!(name in skills)) changes.push({ name, action: "skipped", reason: "not managed by skilllib in this project" });
2863
+ }
2864
+ if (changes.some((c) => c.action === "skipped")) process.exitCode = 1;
2420
2865
  if (!args.json) {
2421
2866
  if (args.all) info(`${basename8(root)} ${dim(tildify(root))}`);
2422
2867
  if (changes.length === 0) success("Everything is on the latest version");
@@ -2424,14 +2869,14 @@ function update(args) {
2424
2869
  }
2425
2870
  return { project: basename8(root), path: root, changes: withDirs(root, changes) };
2426
2871
  });
2427
- if (args.json) json(args.all ? results : results[0].changes);
2872
+ if (args.json) args.all ? json("changes --all", results) : json("changes", results[0].changes);
2428
2873
  }
2429
2874
  function outdated(args) {
2430
- const roots = args.all ? visibleProjects() : [findProjectRoot()];
2875
+ const roots = args.all ? visibleProjects() : [requireProject()];
2431
2876
  const behind = roots.flatMap(
2432
2877
  (root) => projectStatus(root).filter((s) => s.managed && s.latest !== null && s.version !== null && s.latest > s.version).map((s) => ({ project: basename8(root), path: root, skill: s.name, installed: s.version, latest: s.latest, state: s.state }))
2433
2878
  );
2434
- if (args.json) return json(behind);
2879
+ if (args.json) return json("outdated", behind);
2435
2880
  const rows = behind.map((b) => ({ Project: b.project, Skill: b.skill, Installed: `v${b.installed}`, Latest: `v${b.latest}`, Status: colorState(b.state) }));
2436
2881
  if (rows.length === 0) return success("Everything is on the latest version");
2437
2882
  table(rows);
@@ -2439,7 +2884,7 @@ function outdated(args) {
2439
2884
  \u2192 skilllib update${args.all ? " --all" : ""}`));
2440
2885
  }
2441
2886
  function importSkills(args) {
2442
- const dirs = args.global ? machineSkills().filter((m) => m.movable && !m.broken).map((m) => m.path) : args.positional.map((p) => resolve4(p));
2887
+ const dirs = args.global ? machineSkills().filter((m) => m.movable && !m.broken).map((m) => m.path) : args.positional.map((p) => resolve7(p));
2443
2888
  if (dirs.length === 0) {
2444
2889
  error("Usage: skilllib import <skill-dir>... [--force] | skilllib import --global");
2445
2890
  process.exit(1);
@@ -2458,9 +2903,9 @@ function importSkills(args) {
2458
2903
  continue;
2459
2904
  }
2460
2905
  success(`${name} ${dim(result === "unchanged" ? "already in library" : result === "added" ? "added to library" : "library updated")}`);
2461
- const skillsDir = resolve4(dir, "..");
2462
- const root = resolve4(skillsDir, "..", "..");
2463
- if (skillsDir === join15(root, ".claude", "skills") && isProjectCandidate(root)) {
2906
+ const skillsDir = resolve7(dir, "..");
2907
+ const root = resolve7(skillsDir, "..", "..");
2908
+ if (skillsDir === join14(root, ".claude", "skills") && isProjectCandidate(root)) {
2464
2909
  if (readManifest(root).skills[name]?.hash !== treeHash(dir)) {
2465
2910
  const change = addSkill(root, name);
2466
2911
  if (change.action !== "skipped") info(dim(` ${basename8(root)} now depends on ${name} v${change.to}`));
@@ -2468,7 +2913,7 @@ function importSkills(args) {
2468
2913
  rememberProjects([root]);
2469
2914
  }
2470
2915
  }
2471
- if (args.json) json(results);
2916
+ if (args.json) json("import", results);
2472
2917
  }
2473
2918
  function scan(args) {
2474
2919
  for (const p of args.positional) addRoot(p);
@@ -2479,24 +2924,34 @@ function scan(args) {
2479
2924
  }
2480
2925
  const found = discoverProjects();
2481
2926
  const library = new Set(librarySkills().map((s) => s.name));
2482
- const skillNames = (root) => [...skillDirsIn(projectSkillsDir(root)), ...skillDirsIn(agentsSkillsDir(root))].map((d) => basename8(d));
2927
+ const skillNames = (root) => [
2928
+ .../* @__PURE__ */ new Set([...ALL_PROJECT_DIRS.flatMap((d) => skillDirsIn(join14(root, d))).map((d) => basename8(d)), ...nestedSkills(root).map((s) => s.name)])
2929
+ ];
2483
2930
  const withSkills = found.filter((root) => skillNames(root).length > 0);
2484
- info(`Scanned ${roots.map((r) => tildify(r)).join(", ")}: ${found.length} project(s), ${withSkills.length} with skills
2931
+ info(`Scanned ${roots.map((r) => tildify(r)).join(", ")}: ${found.length} project${found.length === 1 ? "" : "s"}, ${withSkills.length} with skills
2485
2932
  `);
2486
2933
  table(
2487
2934
  withSkills.map((root) => ({
2488
2935
  Project: basename8(root),
2489
2936
  Path: dim(tildify(root)),
2490
- Skills: [...new Set(skillNames(root))].map((n) => library.has(n) ? n : yellow(n)).join(", ")
2937
+ Skills: skillNames(root).map((n) => library.has(n) ? n : yellow(n)).join(", ")
2491
2938
  }))
2492
2939
  );
2493
2940
  if (withSkills.length > 0) info(`
2494
- ${yellow("Yellow")} skills aren't in your library yet (import them from \`skilllib\` \u2192 3 Sources or 5 Health).`);
2941
+ ${yellow("Yellow")} skills aren't in your library yet (\`skilllib import <skill-dir>\`, or \`skilllib\` \u2192 Health).`);
2495
2942
  }
2496
2943
  function folders(args) {
2497
2944
  const [action, ...paths] = args.positional;
2945
+ let failed = false;
2498
2946
  if (action === "add") {
2499
- for (const p of paths) success(`Added ${tildify(addRoot(p))}`);
2947
+ for (const p of paths) {
2948
+ try {
2949
+ success(`Added ${tildify(addRoot(p))}`);
2950
+ } catch (e) {
2951
+ error(e.message);
2952
+ failed = true;
2953
+ }
2954
+ }
2500
2955
  discoverProjects();
2501
2956
  } else if (action === "remove" || action === "rm") {
2502
2957
  for (const p of paths) {
@@ -2508,12 +2963,24 @@ function folders(args) {
2508
2963
  info(roots.length ? `Project folders:
2509
2964
  ${roots.map((r) => ` ${tildify(r)}`).join("\n")}` : "No project folders. Add one: skilllib folders add ~/Projects");
2510
2965
  if (hidden.length) info(dim(`Hidden projects: ${hidden.map((h) => tildify(h)).join(", ")} (skilllib unhide <path>)`));
2966
+ if (failed) process.exit(1);
2511
2967
  }
2512
2968
  function unhide(args) {
2969
+ if (args.positional.length === 0) {
2970
+ error("Usage: skilllib unhide <path>...");
2971
+ process.exit(1);
2972
+ }
2973
+ let failed = false;
2513
2974
  for (const p of args.positional) {
2514
- setHidden(expandHome(p), false);
2515
- success(`${tildify(expandHome(p))} is visible again`);
2975
+ const path = expandHome(p);
2976
+ if (setHidden(path, false)) success(`${tildify(path)} is visible again`);
2977
+ else {
2978
+ const { hidden } = readConfig();
2979
+ error(`${tildify(path)} wasn't hidden. ${hidden.length ? `Hidden projects: ${hidden.map((h) => tildify(h)).join(", ")}` : "No projects are hidden."}`);
2980
+ failed = true;
2981
+ }
2516
2982
  }
2983
+ if (failed) process.exit(1);
2517
2984
  }
2518
2985
  function newSkill(args) {
2519
2986
  const [name, ...rest] = args.positional;
@@ -2526,14 +2993,14 @@ function newSkill(args) {
2526
2993
  error(`Can't create ${name}: ${r.reason}`);
2527
2994
  process.exit(1);
2528
2995
  }
2529
- success(`Created ${tildify(join15(r.dir, "SKILL.md"))}`);
2996
+ success(`Created ${tildify(join14(r.dir, "SKILL.md"))}`);
2530
2997
  }
2531
2998
  function restore(args) {
2532
2999
  const backups = [...listBackups(), ...pluginBackups()];
2533
3000
  const name = args.positional[0];
2534
3001
  if (!name) {
2535
3002
  if (backups.length === 0) return info("Nothing to restore.");
2536
- table(backups.map((b) => ({ Skill: b.name, From: b.kind === "trash" ? "library (deleted)" : b.kind === "plugin" ? "Claude Code plugin" : tildify(dirname5(b.from)), Moved: b.movedAt })));
3003
+ table(backups.map((b) => ({ Skill: b.name, From: backupFrom(b), Moved: b.movedAt })));
2537
3004
  info(dim("\n\u2192 skilllib restore <name>"));
2538
3005
  return;
2539
3006
  }
@@ -2544,7 +3011,10 @@ function restore(args) {
2544
3011
  }
2545
3012
  const r = backup.kind === "plugin" ? restorePlugin(backup) : restoreBackup(backup);
2546
3013
  if (r.ok) success(`Restored ${name} to ${tildify(r.to)}`);
2547
- else error(`Can't restore ${name}: ${r.reason}`);
3014
+ else {
3015
+ error(`Can't restore ${name}: ${r.reason}`);
3016
+ process.exitCode = 1;
3017
+ }
2548
3018
  }
2549
3019
  async function doctor(args) {
2550
3020
  const projects2 = visibleProjects();
@@ -2564,14 +3034,25 @@ async function doctor(args) {
2564
3034
  })
2565
3035
  );
2566
3036
  }
3037
+ const fixed = (fix) => {
3038
+ const r = runFix(fix);
3039
+ if (!r.ok) process.exitCode = 1;
3040
+ return r;
3041
+ };
2567
3042
  if (args.json) {
2568
3043
  return json(
2569
- issues.map(({ fix, choices, ...issue }) => ({
2570
- ...issue,
2571
- ...fix && { fix: fix.label },
2572
- ...choices && { choices: choices.map((c) => c.label) },
2573
- ...args.fix && fix && { fixed: runFix(fix) }
2574
- }))
3044
+ "doctor",
3045
+ issues.map(
3046
+ ({ id, severity, title, detail, fix, choices }) => ({
3047
+ id,
3048
+ severity,
3049
+ title,
3050
+ detail,
3051
+ fix: fix?.label,
3052
+ choices: choices?.map((c) => c.label),
3053
+ fixed: args.fix && fix ? fixed(fix) : void 0
3054
+ })
3055
+ )
2575
3056
  );
2576
3057
  }
2577
3058
  if (issues.length === 0) return success("Everything looks good");
@@ -2580,7 +3061,7 @@ async function doctor(args) {
2580
3061
  info(dim(` ${issue.detail}${issue.fix ? ` \u2192 ${issue.fix.label}` : ""}`));
2581
3062
  if (issue.choices) info(dim(` choose in skilllib \u2192 Health: ${issue.choices.map((c) => c.label).join(" / ")}`));
2582
3063
  if (args.fix && issue.fix) {
2583
- const r = runFix(issue.fix);
3064
+ const r = fixed(issue.fix);
2584
3065
  info(` ${r.ok ? green("\u2713") : red("\u2717")} ${r.message}`);
2585
3066
  }
2586
3067
  }
@@ -2590,7 +3071,7 @@ async function usage(args) {
2590
3071
  const summaries = summarize(await scanUsage(args.days), projectOfFactory(knownProjects()));
2591
3072
  if (args.json) {
2592
3073
  const library2 = new Set(librarySkills().map((s) => s.name));
2593
- return json({
3074
+ return json("usage", {
2594
3075
  usageDays: args.days,
2595
3076
  source: "Claude Code transcripts",
2596
3077
  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) })),
@@ -2602,7 +3083,7 @@ async function usage(args) {
2602
3083
  return;
2603
3084
  }
2604
3085
  const library = new Set(librarySkills().map((s) => s.name));
2605
- info(`Skill usage, last ${args.days} days ${dim(`(from ${tildify(join15(claudeDir(), "projects"))})`)}
3086
+ info(`Skill usage, last ${args.days} days ${dim(`(from ${tildify(join14(claudeDir(), "projects"))})`)}
2606
3087
  `);
2607
3088
  table(
2608
3089
  summaries.map((s) => ({
@@ -2621,21 +3102,21 @@ async function usage(args) {
2621
3102
  }
2622
3103
  function show(args) {
2623
3104
  const name = args.positional[0];
2624
- const dir = name ? join15(libraryDir(), name) : "";
3105
+ const dir = name ? join14(libraryDir(), name) : "";
2625
3106
  if (!name || !isSkillDir(dir)) {
2626
3107
  error(name ? `${name} is not in the library` : "Usage: skilllib show <skill-name>");
2627
3108
  process.exit(1);
2628
3109
  }
2629
3110
  const { description } = readSkillInfo(dir);
2630
3111
  if (args.json) {
2631
- return json({
3112
+ return json("show", {
2632
3113
  name,
2633
3114
  description,
2634
3115
  path: dir,
2635
3116
  latest: latestVersion(name)?.version ?? null,
2636
3117
  origin: libraryOrigins()[name] ?? "",
2637
3118
  projects: knownProjects().filter((p) => name in readManifest(p).skills).map((p) => basename8(p)),
2638
- skillMd: readFileSync9(join15(dir, "SKILL.md"), "utf-8")
3119
+ skillMd: readFileSync9(join14(dir, "SKILL.md"), "utf-8")
2639
3120
  });
2640
3121
  }
2641
3122
  info(`${name} ${dim(tildify(dir))}`);
@@ -2664,14 +3145,21 @@ function harnessesCommand(args) {
2664
3145
  New installs go to ${installDirs(enabled).join(" (real copy) + ")}${installDirs(enabled).length > 1 ? " (link)" : ""}.`));
2665
3146
  }
2666
3147
  function link(args) {
2667
- const roots = args.all ? visibleProjects() : [findProjectRoot()];
3148
+ const roots = args.all ? visibleProjects() : [requireProject()];
2668
3149
  for (const root of roots) {
2669
3150
  if (args.allowTracked) allowTrackedLinks(root);
2670
3151
  const res = linkAll(root);
2671
3152
  const label = args.all ? `${basename8(root)}: ` : "";
2672
3153
  if (res.linked.length) success(`${label}linked ${res.linked.length} skill(s) for your other agents`);
2673
- else if (!args.all) success("Every skill here is already usable by all your agents");
2674
- if (res.blocked.length) warn(`${label}git tracks ${res.blocked.join(", ")}; re-run with --allow-tracked to link there too`);
3154
+ else if (!args.all && !res.blocked.length && !res.uncommitted.length) success("Every skill here is already usable by all your agents");
3155
+ if (res.blocked.length) {
3156
+ process.exitCode = 1;
3157
+ 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`);
3158
+ }
3159
+ if (res.uncommitted.length)
3160
+ warn(
3161
+ `${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`
3162
+ );
2675
3163
  }
2676
3164
  }
2677
3165
  function globalCommand(args) {
@@ -2692,14 +3180,41 @@ function globalCommand(args) {
2692
3180
  const kept = keptGlobal();
2693
3181
  if (!yours.length) return info("None of your skills load globally.");
2694
3182
  for (const m of yours) info(`${kept.has(m.name) ? green("\u2713") : yellow("\u26A0")} ${m.name.padEnd(32)} ${dim(tildify(m.path))}`);
2695
- const unreviewed = yours.filter((m) => !kept.has(m.name)).length;
3183
+ const unreviewed = new Set(yours.filter((m) => !kept.has(m.name)).map((m) => m.name)).size;
2696
3184
  if (unreviewed) info(dim(`
2697
3185
  ${unreviewed} not reviewed. Keep one global on purpose: skilllib global keep <name>`));
2698
3186
  }
2699
3187
  function tidy(args) {
2700
3188
  const names = args.positional;
2701
- const keepFor = (base) => args.keep ? Object.fromEntries(names.map((n) => [n, base ? resolve4(base, args.keep) : expandHome(args.keep)])) : {};
2702
- const reports = args.global ? [{ label: "global folders", report: planGlobalTidy({ keep: keepFor(null) }) }] : (args.all ? visibleProjects() : [findProjectRoot()]).map((root) => ({ label: basename8(root), report: planProjectTidy(root, { keep: keepFor(root) }) }));
3189
+ const keepFor = (base) => args.keep ? Object.fromEntries(names.map((n) => [n, base ? resolve7(base, args.keep) : expandHome(args.keep)])) : {};
3190
+ if (args.keep && !names.length) {
3191
+ error("Usage: skilllib tidy <name>... --keep <folder>");
3192
+ process.exit(1);
3193
+ }
3194
+ const targets = args.global ? [null] : args.all ? visibleProjects() : [requireProject()];
3195
+ const problems = [];
3196
+ for (const name of names) {
3197
+ const where = targets.map((root) => ({ root, ...foldersOf(root, name) })).filter((w) => w.all.length);
3198
+ if (!where.length) {
3199
+ problems.push(`${name}: no skill by that name ${args.global ? "in your global folders" : args.all ? "in your projects" : `in ${basename8(targets[0])}`}`);
3200
+ continue;
3201
+ }
3202
+ if (!args.keep) continue;
3203
+ for (const { root, real } of where) {
3204
+ if (real.includes(keepFor(root)[name])) continue;
3205
+ const label = root ? `${basename8(root)} \xB7 ` : "";
3206
+ problems.push(
3207
+ `${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"}`
3208
+ );
3209
+ }
3210
+ }
3211
+ if (problems.length) {
3212
+ for (const p of problems) error(p);
3213
+ process.exit(1);
3214
+ }
3215
+ const reports = targets.map(
3216
+ (root) => root === null ? { label: "global folders", report: planGlobalTidy({ keep: keepFor(null) }) } : { label: basename8(root), report: planProjectTidy(root, { keep: keepFor(root) }) }
3217
+ );
2703
3218
  let held = 0;
2704
3219
  let changed = 0;
2705
3220
  for (const { label, report } of reports) {
@@ -2714,6 +3229,7 @@ function tidy(args) {
2714
3229
  held += args.dryRun ? 0 : result.held.length;
2715
3230
  changed += result.applied.length;
2716
3231
  }
3232
+ if (args.keep && conflicts.length) process.exitCode = 1;
2717
3233
  for (const c of conflicts) {
2718
3234
  warn(`${label} \xB7 ${c.name}: the copies differ`);
2719
3235
  for (const copy of c.copies) {
@@ -2737,7 +3253,7 @@ function agentSkill(args) {
2737
3253
  error(`Can't install the skilllib skill: ${r.reason}`);
2738
3254
  process.exit(1);
2739
3255
  }
2740
- if (args.json) return json({ state: agentSkillState(), dirs: r.dirs });
3256
+ if (args.json) return json("agent-skill", { state: agentSkillState(), dirs: r.dirs });
2741
3257
  success(`Installed in ${r.dirs.map((d) => tildify(d)).join(" and ")}`);
2742
3258
  if (!onPath("skilllib")) warn("`skilllib` isn't on your PATH, so agents can't run it. Install it: npm install -g skilllib");
2743
3259
  info(dim("New agent sessions can now answer questions like \u201Cwhich skills can you use here?\u201D"));
@@ -2745,27 +3261,30 @@ function agentSkill(args) {
2745
3261
  }
2746
3262
  if (action === "remove" || action === "rm") {
2747
3263
  const removed = removeAgentSkill();
2748
- if (args.json) return json({ state: agentSkillState(), removed });
3264
+ if (args.json) return json("agent-skill", { state: agentSkillState(), removed });
2749
3265
  return removed.length ? success(`Removed from ${removed.map((d) => tildify(d)).join(" and ")}`) : info("The skilllib skill isn't installed.");
2750
3266
  }
2751
3267
  const state = agentSkillState();
2752
- if (args.json) return json({ state, dirs: agentSkillDirs() });
3268
+ if (args.json) return json("agent-skill", { state, dirs: agentSkillDirs() });
2753
3269
  if (state === "installed") success(`Your agents can use skilllib ${dim(`(${agentSkillDirs().map((d) => tildify(d)).join(", ")})`)}`);
2754
3270
  else if (state === "outdated") warn("The skilllib skill is out of date. Run `skilllib agent-skill install` to update it.");
2755
3271
  else info("Your agents don't know about skilllib yet. Run `skilllib agent-skill install` so you can ask them about your skills.");
2756
3272
  }
2757
3273
 
2758
3274
  export {
2759
- userHome,
2760
3275
  skilllibHome,
3276
+ PROJECT_SKILLS_DIR,
2761
3277
  HARNESSES,
2762
3278
  harness,
2763
3279
  readSkillInfo,
2764
3280
  versionHistory,
2765
3281
  forgetLatest,
2766
3282
  latestVersion,
2767
- findProjectRoot,
2768
- isProjectCandidate,
3283
+ projectHere,
3284
+ readManifest,
3285
+ setJsonMode,
3286
+ error,
3287
+ tildify,
2769
3288
  readConfig,
2770
3289
  addRoot,
2771
3290
  removeRoot,
@@ -2778,13 +3297,13 @@ export {
2778
3297
  allowTrackedLinks,
2779
3298
  keptGlobal,
2780
3299
  setKeepGlobal,
2781
- setPluginEnabled,
2782
3300
  machineSkills,
2783
3301
  skillsLoadedIn,
2784
3302
  libraryOrigins,
2785
3303
  recordOrigin,
2786
3304
  librarySkills,
2787
3305
  librarySkillDir,
3306
+ visibilityOf,
2788
3307
  projectStatus,
2789
3308
  linkEverywhere,
2790
3309
  relinkDependency,
@@ -2802,17 +3321,23 @@ export {
2802
3321
  createSkill,
2803
3322
  listBackups,
2804
3323
  restoreBackup,
2805
- setJsonMode,
2806
- error,
2807
3324
  gitInfo,
2808
3325
  relativeTo,
2809
3326
  claudePlugins,
3327
+ installedPlugins,
3328
+ cursorPlugins,
3329
+ forgetClaudeBinary,
2810
3330
  claudeBinary,
2811
3331
  turnOffIn,
2812
- recordRemovedPlugin,
3332
+ setPluginOn,
3333
+ uninstallPlugin,
3334
+ updatePlugin,
2813
3335
  pluginBackups,
2814
3336
  restorePlugin,
2815
3337
  cursorPluginSkills,
3338
+ agentSkillState,
3339
+ installAgentSkill,
3340
+ removeAgentSkill,
2816
3341
  refreshAgentSkill,
2817
3342
  planProjectTidy,
2818
3343
  planGlobalTidy,
@@ -2820,10 +3345,12 @@ export {
2820
3345
  applyTidy,
2821
3346
  describeStep,
2822
3347
  copyLabel,
3348
+ addedAt,
2823
3349
  scanUsage,
2824
3350
  parseArgs,
2825
3351
  projectOfFactory,
2826
3352
  status,
3353
+ overview,
2827
3354
  list,
2828
3355
  projects,
2829
3356
  add,