skilllib 1.11.2 → 1.11.3

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.
@@ -2,9 +2,9 @@
2
2
  import { resolve as resolve2, sep } from "path";
3
3
 
4
4
  // src/paths.ts
5
- import { realpathSync } from "fs";
5
+ import { mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "fs";
6
6
  import { homedir } from "os";
7
- import { join, resolve } from "path";
7
+ import { dirname, join, resolve } from "path";
8
8
  function userHome() {
9
9
  return process.env.HOME || homedir();
10
10
  }
@@ -41,6 +41,46 @@ function isRepoSkillLocation(location) {
41
41
  return location === AGENTS_SKILLS_DIR || location === GROK_SKILLS_DIR;
42
42
  }
43
43
  var OWN_SKILL_MARKER = ".managed-by-skilllib";
44
+ function readJson(path) {
45
+ try {
46
+ return JSON.parse(readFileSync(path, "utf-8"));
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+ function realpathOrNull(path) {
52
+ try {
53
+ return realpathSync(path);
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+ function readState(path) {
59
+ let text;
60
+ try {
61
+ text = readFileSync(path, "utf-8");
62
+ } catch (err) {
63
+ if (err.code === "ENOENT") return null;
64
+ throw err;
65
+ }
66
+ try {
67
+ return JSON.parse(text);
68
+ } catch {
69
+ throw new Error(`${path} isn't valid JSON: fix it, or move it away to start that file over`);
70
+ }
71
+ }
72
+ function writeState(path, data) {
73
+ const target = realpathOrNull(path) ?? path;
74
+ mkdirSync(dirname(target), { recursive: true });
75
+ const temp = `${target}.${process.pid}.tmp`;
76
+ try {
77
+ writeFileSync(temp, JSON.stringify(data, null, 2) + "\n", { mode: statSync(target, { throwIfNoEntry: false })?.mode ?? 420 });
78
+ renameSync(temp, target);
79
+ } catch (err) {
80
+ rmSync(temp, { force: true });
81
+ throw err;
82
+ }
83
+ }
44
84
 
45
85
  // src/output.ts
46
86
  var jsonMode = false;
@@ -71,21 +111,25 @@ function truncate(s, max) {
71
111
  return line.length > max ? line.slice(0, max - 1) + "\u2026" : line;
72
112
  }
73
113
  var visible = (s) => s.replace(/\x1b\[\d+m/g, "");
74
- function table(rows) {
75
- if (rows.length === 0) return;
76
- const keys = Object.keys(rows[0]);
77
- const widths = keys.map((k) => Math.max(k.length, ...rows.map((r) => visible(r[k] ?? "").length)));
114
+ function table(rows2) {
115
+ if (rows2.length === 0) return;
116
+ const keys = Object.keys(rows2[0]);
117
+ const widths = keys.map((k) => Math.max(k.length, ...rows2.map((r) => visible(r[k] ?? "").length)));
78
118
  const pad = (s, w) => s + " ".repeat(Math.max(0, w - visible(s).length));
79
119
  info(dim(keys.map((k, i) => pad(k, widths[i])).join(" ")));
80
- for (const row of rows) info(keys.map((k, i) => pad(row[k] ?? "", widths[i])).join(" ").trimEnd());
120
+ for (const row of rows2) info(keys.map((k, i) => pad(row[k] ?? "", widths[i])).join(" ").trimEnd());
121
+ }
122
+ function localStamp(ms) {
123
+ const at = new Date(ms);
124
+ return new Date(ms - at.getTimezoneOffset() * 6e4).toISOString().slice(0, 16).replace("T", " ");
81
125
  }
82
126
 
83
127
  // src/agentSkill.ts
84
- import { existsSync as existsSync8, lstatSync as lstatSync3, mkdirSync as mkdirSync6, readFileSync as readFileSync7, rmSync as rmSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync6 } from "fs";
128
+ import { existsSync as existsSync8, lstatSync as lstatSync3, mkdirSync as mkdirSync5, readFileSync as readFileSync4, rmSync as rmSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
85
129
  import { join as join9 } from "path";
86
130
 
87
131
  // src/config.ts
88
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
132
+ import { existsSync as existsSync5, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
89
133
  import { homedir as homedir2 } from "os";
90
134
  import { join as join6, resolve as resolve4 } from "path";
91
135
 
@@ -185,16 +229,16 @@ function installDirs(enabled, keep) {
185
229
  }
186
230
 
187
231
  // src/project.ts
188
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
189
- import { dirname, join as join5, resolve as resolve3 } from "path";
232
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
233
+ import { dirname as dirname2, join as join5, resolve as resolve3 } from "path";
190
234
 
191
235
  // src/versions.ts
192
- import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
236
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
193
237
  import { join as join4 } from "path";
194
238
 
195
239
  // src/skills.ts
196
240
  import { createHash } from "crypto";
197
- import { cpSync, existsSync as existsSync2, readdirSync, readFileSync, realpathSync as realpathSync2, rmSync, statSync } from "fs";
241
+ import { cpSync, existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, realpathSync as realpathSync2, rmSync as rmSync2, statSync as statSync2 } from "fs";
198
242
  import { basename, join as join3, relative, sep as sep2 } from "path";
199
243
  import { parse as parseYaml } from "yaml";
200
244
  var IGNORED = /* @__PURE__ */ new Set([".DS_Store", ".git", "node_modules"]);
@@ -202,7 +246,7 @@ function readSkillInfo(dir) {
202
246
  const skillMd = join3(dir, "SKILL.md");
203
247
  const fallback = { name: basename(dir), description: "" };
204
248
  if (!existsSync2(skillMd)) return fallback;
205
- const match = readFileSync(skillMd, "utf-8").match(/^---\r?\n([\s\S]*?)\r?\n---/);
249
+ const match = readFileSync2(skillMd, "utf-8").match(/^---\r?\n([\s\S]*?)\r?\n---/);
206
250
  if (!match) return fallback;
207
251
  try {
208
252
  const fm = parseYaml(match[1]);
@@ -220,22 +264,22 @@ function isSkillDir(dir) {
220
264
  function listFiles(dir) {
221
265
  return readdirSync(dir).filter((entry) => !IGNORED.has(entry)).flatMap((entry) => {
222
266
  const full = join3(dir, entry);
223
- const st = statSync(full, { throwIfNoEntry: false });
267
+ const st = statSync2(full, { throwIfNoEntry: false });
224
268
  return !st ? [] : st.isDirectory() ? listFiles(full) : [full];
225
269
  });
226
270
  }
227
271
  function treeHash(dir) {
228
272
  if (!existsSync2(dir)) return null;
229
273
  const hash = createHash("sha256");
230
- const files = listFiles(dir).map((f) => ({ path: relative(dir, f).split(sep2).join("/"), full: f })).sort((a, b) => a.path.localeCompare(b.path));
274
+ const files = listFiles(dir).map((f) => ({ path: relative(dir, f).split(sep2).join("/"), full: f })).sort((a, b) => a.path.localeCompare(b.path, "en"));
231
275
  for (const file of files) {
232
- hash.update(file.path).update("\0").update(createHash("sha256").update(readFileSync(file.full)).digest("hex")).update("\n");
276
+ hash.update(file.path).update("\0").update(createHash("sha256").update(readFileSync2(file.full)).digest("hex")).update("\n");
233
277
  }
234
278
  return hash.digest("hex").slice(0, 16);
235
279
  }
236
280
  function copySkill(from, to) {
237
281
  const source = realpathSync2(from);
238
- rmSync(to, { recursive: true, force: true });
282
+ rmSync2(to, { recursive: true, force: true });
239
283
  cpSync(source, to, { recursive: true, dereference: true, filter: (src) => !IGNORED.has(basename(src)) && existsSync2(src) });
240
284
  }
241
285
  function skillDirsIn(dir) {
@@ -251,11 +295,7 @@ function indexFile(name) {
251
295
  return join4(storeDir(name), "versions.json");
252
296
  }
253
297
  function versionHistory(name) {
254
- try {
255
- return JSON.parse(readFileSync2(indexFile(name), "utf-8"));
256
- } catch {
257
- return [];
258
- }
298
+ return readState(indexFile(name)) ?? [];
259
299
  }
260
300
  function versionDir(name, version) {
261
301
  return join4(storeDir(name), String(version));
@@ -277,9 +317,10 @@ function latestVersion(name) {
277
317
  result = last;
278
318
  } else {
279
319
  const next = { version: (last?.version ?? 0) + 1, hash, date: (/* @__PURE__ */ new Date()).toISOString() };
280
- mkdirSync(storeDir(name), { recursive: true });
320
+ if (existsSync3(versionDir(name, next.version))) throw new Error(`${versionDir(name, next.version)} exists but ${indexFile(name)} doesn't list it: fix the index before adding versions`);
321
+ mkdirSync2(storeDir(name), { recursive: true });
281
322
  copySkill(source, versionDir(name, next.version));
282
- writeFileSync(indexFile(name), JSON.stringify([...history, next], null, 2) + "\n");
323
+ writeState(indexFile(name), [...history, next]);
283
324
  result = next;
284
325
  }
285
326
  }
@@ -298,7 +339,7 @@ function findProjectRoot(from = process.cwd()) {
298
339
  let dir = resolve3(from);
299
340
  while (true) {
300
341
  if (existsSync4(join5(dir, MANIFEST_FILE)) || existsSync4(join5(dir, ".git"))) return dir;
301
- const parent = dirname(dir);
342
+ const parent = dirname2(dir);
302
343
  if (parent === dir) return resolve3(from);
303
344
  dir = parent;
304
345
  }
@@ -310,7 +351,12 @@ function projectHere(from = process.cwd()) {
310
351
  function readManifest(root) {
311
352
  const path = join5(root, MANIFEST_FILE);
312
353
  if (!existsSync4(path)) return { skills: {} };
313
- const data = JSON.parse(readFileSync3(path, "utf-8"));
354
+ let data;
355
+ try {
356
+ data = JSON.parse(readFileSync3(path, "utf-8"));
357
+ } catch {
358
+ throw new Error(`${path} isn't valid JSON (a merge conflict?): fix it, then run skilllib again`);
359
+ }
314
360
  const skills = Object.fromEntries(
315
361
  Object.entries(data.skills ?? {}).flatMap(([name, value]) => {
316
362
  if (name.startsWith("@")) return [];
@@ -335,8 +381,8 @@ function readManifest(root) {
335
381
  return { ...data, skills };
336
382
  }
337
383
  function writeManifest(root, manifest) {
338
- const skills = Object.fromEntries(Object.entries(manifest.skills).sort(([a], [b]) => a.localeCompare(b)));
339
- writeFileSync2(join5(root, MANIFEST_FILE), JSON.stringify({ ...manifest, skills }, null, 2) + "\n");
384
+ const skills = Object.fromEntries(Object.entries(manifest.skills).sort(([a], [b]) => a.localeCompare(b, "en")));
385
+ writeState(join5(root, MANIFEST_FILE), { ...manifest, skills });
340
386
  }
341
387
  function projectsFile() {
342
388
  return join5(skilllibHome(), "projects.json");
@@ -344,7 +390,7 @@ function projectsFile() {
344
390
  function knownProjects() {
345
391
  const path = projectsFile();
346
392
  if (!existsSync4(path)) return [];
347
- const list2 = JSON.parse(readFileSync3(path, "utf-8"));
393
+ const list2 = readState(path) ?? [];
348
394
  return onePerFolder(list2.filter((p) => existsSync4(p) && isProjectCandidate(p) && !isAgentWorktree(p)));
349
395
  }
350
396
  function isAgentWorktree(path) {
@@ -365,8 +411,27 @@ function isProjectCandidate(root) {
365
411
  function rememberProjects(roots) {
366
412
  const incoming = roots.map((r) => resolve3(r)).filter((r) => isProjectCandidate(r) && !isAgentWorktree(r));
367
413
  const all = onePerFolder([...knownProjects(), ...incoming]).sort();
368
- mkdirSync2(skilllibHome(), { recursive: true });
369
- writeFileSync2(projectsFile(), JSON.stringify(all, null, 2) + "\n");
414
+ writeState(projectsFile(), all);
415
+ }
416
+ function readableRoots(roots) {
417
+ const unreadable = [];
418
+ const readable = roots.filter((root) => {
419
+ try {
420
+ readManifest(root);
421
+ return true;
422
+ } catch (err) {
423
+ unreadable.push({ root, reason: err.message });
424
+ return false;
425
+ }
426
+ });
427
+ return { readable, unreadable };
428
+ }
429
+ function pinsIn(root) {
430
+ try {
431
+ return readManifest(root).skills;
432
+ } catch {
433
+ return {};
434
+ }
370
435
  }
371
436
 
372
437
  // src/config.ts
@@ -374,16 +439,11 @@ function configFile() {
374
439
  return join6(skilllibHome(), "config.json");
375
440
  }
376
441
  function readConfig() {
377
- try {
378
- const data = JSON.parse(readFileSync4(configFile(), "utf-8"));
379
- return { ...data, roots: data.roots ?? [], hidden: data.hidden ?? [] };
380
- } catch {
381
- return { roots: [], hidden: [] };
382
- }
442
+ const data = readState(configFile()) ?? {};
443
+ return { ...data, roots: data.roots ?? [], hidden: data.hidden ?? [] };
383
444
  }
384
445
  function writeConfig(config) {
385
- mkdirSync3(skilllibHome(), { recursive: true });
386
- writeFileSync3(configFile(), JSON.stringify(config, null, 2) + "\n");
446
+ writeState(configFile(), config);
387
447
  }
388
448
  function expandHome(path) {
389
449
  return resolve4(path.replace(/^~(?=$|\/)/, homedir2()));
@@ -391,7 +451,7 @@ function expandHome(path) {
391
451
  function addRoot(path) {
392
452
  const root = expandHome(path);
393
453
  if (!existsSync5(root)) throw new Error(`${tildify(root)} doesn't exist`);
394
- if (!statSync2(root).isDirectory()) throw new Error(`${tildify(root)} isn't a folder`);
454
+ if (!statSync3(root).isDirectory()) throw new Error(`${tildify(root)} isn't a folder`);
395
455
  const config = readConfig();
396
456
  if (!config.roots.includes(root)) writeConfig({ ...config, roots: [...config.roots, root].sort() });
397
457
  return root;
@@ -464,35 +524,25 @@ function setKeepGlobal(names, keep) {
464
524
 
465
525
  // src/library.ts
466
526
  import { execFileSync } from "child_process";
467
- import { cpSync as cpSync2, 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";
468
- import { basename as basename3, dirname as dirname2, join as join8, relative as relative2, sep as sep3 } from "path";
527
+ import { cpSync as cpSync2, existsSync as existsSync7, lstatSync as lstatSync2, mkdirSync as mkdirSync4, readdirSync as readdirSync4, renameSync as renameSync2, rmSync as rmSync3, symlinkSync, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
528
+ import { basename as basename3, dirname as dirname3, join as join8, relative as relative2, sep as sep4 } from "path";
469
529
 
470
530
  // src/sources.ts
471
- import { existsSync as existsSync6, lstatSync, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync5, readlinkSync, realpathSync as realpathSync3, writeFileSync as writeFileSync4 } from "fs";
472
- import { basename as basename2, join as join7, resolve as resolve5 } from "path";
473
- function readJson(path) {
474
- try {
475
- return JSON.parse(readFileSync5(path, "utf-8"));
476
- } catch {
477
- return null;
478
- }
479
- }
531
+ import { existsSync as existsSync6, lstatSync, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readlinkSync } from "fs";
532
+ import { basename as basename2, join as join7, resolve as resolve5, sep as sep3 } from "path";
533
+ import { tmpdir } from "os";
480
534
  function skillsShLock() {
481
535
  return readJson(join7(userHome(), ".agents", ".skill-lock.json"))?.skills ?? {};
482
536
  }
483
537
  function projectSkillsLock(root) {
484
538
  return readJson(join7(root, "skills-lock.json"))?.skills ?? {};
485
539
  }
486
- function realpathOrNull(path) {
487
- try {
488
- return realpathSync3(path);
489
- } catch {
490
- return null;
491
- }
540
+ function vendorSkill(path, kind, origin, harnesses) {
541
+ return { name: basename2(path), kind, origin, path, description: readSkillInfo(path).description, movable: false, broken: false, harnesses, links: [] };
492
542
  }
493
543
  function harnessesReading(dir, enabled) {
494
544
  const real = realPath(dir);
495
- return enabled.filter((id) => HARNESSES.find((h) => h.id === id)?.globalDirs().some((d) => realPath(d) === real));
545
+ return enabled.filter((id) => harness(id).globalDirs().some((d) => realPath(d) === real));
496
546
  }
497
547
  var systemDirs = () => [codexSystemDir()];
498
548
  function globalSkillDirs() {
@@ -511,17 +561,7 @@ function globalDirAs(dir) {
511
561
  }
512
562
  function systemSkills(enabled) {
513
563
  return systemDirs().flatMap(
514
- (dir) => skillDirsIn(dir).map((path) => ({
515
- name: basename2(path),
516
- kind: "system",
517
- origin: dir.replace(/\\/g, "/"),
518
- path,
519
- description: readSkillInfo(path).description,
520
- movable: false,
521
- broken: false,
522
- harnesses: harnessesReading(dir, enabled),
523
- links: []
524
- }))
564
+ (dir) => skillDirsIn(dir).map((path) => vendorSkill(path, "system", dir.replace(/\\/g, "/"), harnessesReading(dir, enabled)))
525
565
  ).filter((s) => s.harnesses.length > 0);
526
566
  }
527
567
  function globalFolderSkills(enabled) {
@@ -550,7 +590,7 @@ function globalFolderSkills(enabled) {
550
590
  const inAgents = e.dir === agentsDir || target_.includes(".agents/skills");
551
591
  const locked = lock[e.name]?.source;
552
592
  if (existsSync6(join7(e.path, OWN_SKILL_MARKER))) {
553
- 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: [] });
593
+ skills.push(vendorSkill(e.path, "skilllib", "skilllib", harnessesReading(e.dir, enabled)));
554
594
  continue;
555
595
  }
556
596
  skills.push({
@@ -578,31 +618,11 @@ function tildify2(p) {
578
618
  }
579
619
  function grokBuiltInSkills(enabled) {
580
620
  if (!enabled.includes("grok")) return [];
581
- return skillDirsIn(join7(grokDir(), "bundled", "skills")).map((path) => ({
582
- name: basename2(path),
583
- kind: "built-in",
584
- origin: "Grok",
585
- path,
586
- description: readSkillInfo(path).description,
587
- movable: false,
588
- broken: false,
589
- harnesses: ["grok"],
590
- links: []
591
- }));
621
+ return skillDirsIn(join7(grokDir(), "bundled", "skills")).map((path) => vendorSkill(path, "built-in", "Grok", ["grok"]));
592
622
  }
593
623
  function cursorBuiltInSkills(enabled) {
594
624
  if (!enabled.includes("cursor")) return [];
595
- return skillDirsIn(join7(userHome(), ".cursor", "skills-cursor")).map((path) => ({
596
- name: basename2(path),
597
- kind: "built-in",
598
- origin: "Cursor",
599
- path,
600
- description: readSkillInfo(path).description,
601
- movable: false,
602
- broken: false,
603
- harnesses: ["cursor"],
604
- links: []
605
- }));
625
+ return skillDirsIn(join7(userHome(), ".cursor", "skills-cursor")).map((path) => vendorSkill(path, "built-in", "Cursor", ["cursor"]));
606
626
  }
607
627
  function claudeAiSkills(enabled) {
608
628
  const byReal = /* @__PURE__ */ new Map();
@@ -617,20 +637,13 @@ function claudeAiSkills(enabled) {
617
637
  }
618
638
  return [...byReal.values()].flatMap(
619
639
  ({ dir, readers }) => readdirSync3(dir).filter((d) => !d.startsWith(".")).flatMap(
620
- (bucket) => skillDirsIn(join7(dir, bucket)).map((path) => ({
621
- name: basename2(path),
622
- kind: "claude.ai",
623
- origin: "claude.ai account",
624
- path,
625
- description: readSkillInfo(path).description,
626
- movable: false,
627
- broken: false,
628
- harnesses: enabled.filter((id) => readers.includes(id)),
629
- links: []
630
- }))
640
+ (bucket) => skillDirsIn(join7(dir, bucket)).map((path) => vendorSkill(path, "claude.ai", "claude.ai account", enabled.filter((id) => readers.includes(id))))
631
641
  )
632
642
  );
633
643
  }
644
+ function installedPluginsJson() {
645
+ return readJson(join7(claudeDir(), "plugins", "installed_plugins.json"))?.plugins ?? {};
646
+ }
634
647
  function pluginDirCandidates(id, installPaths) {
635
648
  const [name = "", marketplace = ""] = id.split("@");
636
649
  const plugins = join7(claudeDir(), "plugins");
@@ -653,7 +666,7 @@ function pluginSkills(harnesses, root) {
653
666
  const cursor = harnesses.includes("cursor");
654
667
  if (!claude2 && !cursor) return [];
655
668
  const plugins = join7(claudeDir(), "plugins");
656
- const installed = readJson(join7(plugins, "installed_plugins.json"))?.plugins ?? {};
669
+ const installed = installedPluginsJson();
657
670
  const user = enabledPlugins();
658
671
  const enabled = root ? enabledPlugins(root) : user;
659
672
  const fromMarketplaces = [.../* @__PURE__ */ new Set([...Object.keys(enabled), ...Object.keys(user)])].filter((id) => claude2 && enabled[id] || cursor && user[id]).flatMap((id) => {
@@ -673,17 +686,7 @@ function pluginSkills(harnesses, root) {
673
686
  return skillDirsIn(join7(root2, "skills")).map((path) => ({ path, origin: `${name}@synced`, loadedBy: ["claude-code"] }));
674
687
  })
675
688
  ) : [];
676
- return [...fromMarketplaces, ...fromSynced].map(({ path, origin, loadedBy }) => ({
677
- name: basename2(path),
678
- kind: "plugin",
679
- origin,
680
- path,
681
- description: readSkillInfo(path).description,
682
- movable: false,
683
- broken: false,
684
- harnesses: loadedBy,
685
- links: []
686
- }));
689
+ return [...fromMarketplaces, ...fromSynced].map(({ path, origin, loadedBy }) => vendorSkill(path, "plugin", origin, loadedBy));
687
690
  }
688
691
  var byKindAndName = (a, b) => a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name);
689
692
  function machineSkills(enabled = enabledHarnesses()) {
@@ -705,15 +708,18 @@ function originsFile() {
705
708
  return join7(skilllibHome(), "origins.json");
706
709
  }
707
710
  function libraryOrigins() {
708
- return readJson(originsFile()) ?? {};
711
+ return readState(originsFile()) ?? {};
709
712
  }
710
713
  function recordOrigin(name, origin) {
711
714
  const origins = libraryOrigins();
712
715
  if (origins[name] === origin) return;
713
- mkdirSync4(skilllibHome(), { recursive: true });
714
- writeFileSync4(originsFile(), JSON.stringify({ ...origins, [name]: origin }, null, 2) + "\n");
716
+ mkdirSync3(skilllibHome(), { recursive: true });
717
+ writeState(originsFile(), { ...origins, [name]: origin });
715
718
  }
716
719
  function originFor(dir) {
720
+ const under = (path, folder) => path.startsWith((realpathOrNull(folder) ?? folder) + sep3);
721
+ const real = realpathOrNull(dir) ?? dir;
722
+ if ([tmpdir(), "/tmp", "/var/tmp"].some((t) => under(real, t)) && !under(real, userHome())) return "";
717
723
  const match = machineSkills().find((s) => resolve5(s.path) === resolve5(dir));
718
724
  if (match) return match.kind === "global" ? "global folder" : `${match.kind}: ${match.origin}`;
719
725
  const project = dir.split(/[\\/]\.(?:claude|agents|grok|cursor|codex)[\\/]skills[\\/]/)[0];
@@ -733,13 +739,6 @@ function librarySkills() {
733
739
  function librarySkillDir(name) {
734
740
  return join8(libraryDir(), name);
735
741
  }
736
- function realpathOrNull2(path) {
737
- try {
738
- return realpathSync4(path);
739
- } catch {
740
- return null;
741
- }
742
- }
743
742
  function isLink(path) {
744
743
  try {
745
744
  return lstatSync2(path).isSymbolicLink();
@@ -749,7 +748,7 @@ function isLink(path) {
749
748
  }
750
749
  function linkDir(target, link2) {
751
750
  if (process.platform === "win32") symlinkSync(target, link2, "junction");
752
- else symlinkSync(relative2(dirname2(link2), target), link2);
751
+ else symlinkSync(relative2(dirname3(link2), target), link2);
753
752
  }
754
753
  function entryExists(path) {
755
754
  return isLink(path) || existsSync7(path);
@@ -761,9 +760,9 @@ function visibilityOf(root, name, enabled = enabledHarnesses()) {
761
760
  }));
762
761
  }
763
762
  function dependencyDirs(root, name, dep) {
764
- const wanted = installDirs(enabledHarnesses());
765
763
  const untracked = ALL_PROJECT_DIRS.find((d) => d !== GROK_SKILLS_DIR && existsSync7(join8(root, d, name, "SKILL.md")) && !isLink(join8(root, d, name)));
766
- const primary = dep ? dep.dir ?? PROJECT_SKILLS_DIR : untracked ?? wanted[0];
764
+ const primary = dep ? dep.dir ?? PROJECT_SKILLS_DIR : untracked ?? installDirs(enabledHarnesses())[0];
765
+ const wanted = installDirs(enabledHarnesses(), primary);
767
766
  return [primary, .../* @__PURE__ */ new Set([...dep?.links ?? [], ...wanted.filter((d) => d !== primary)])];
768
767
  }
769
768
  function isGitTracked(root, dir) {
@@ -809,7 +808,7 @@ function projectStatus(root) {
809
808
  }
810
809
  for (const [name, paths] of byName) {
811
810
  const real = paths.find((p) => !isLink(p)) ?? paths[0];
812
- const location = relative2(root, join8(real, "..")).split(sep3).join("/");
811
+ const location = relative2(root, join8(real, "..")).split(sep4).join("/");
813
812
  const source = lock[name]?.source;
814
813
  unmanaged.push({
815
814
  name,
@@ -854,9 +853,9 @@ function nestedSkills(root, { depth = 3, enabled = enabledHarnesses() } = {}) {
854
853
  for (const sub of subs) {
855
854
  if (existsSync7(join8(sub, ".git"))) continue;
856
855
  for (const [skillsDir, readers] of NESTED_READERS) {
857
- const location = relative2(root, join8(sub, skillsDir)).split(sep3).join("/");
856
+ const location = relative2(root, join8(sub, skillsDir)).split(sep4).join("/");
858
857
  for (const path of skillDirsIn(join8(sub, skillsDir))) {
859
- if (isLink(path) && (realpathOrNull2(path) ?? "").startsWith((realpathOrNull2(root) ?? root) + sep3)) continue;
858
+ if (isLink(path) && (realpathOrNull(path) ?? "").startsWith((realpathOrNull(root) ?? root) + sep4)) continue;
860
859
  const name = basename3(path);
861
860
  found.push({
862
861
  name,
@@ -907,7 +906,7 @@ function linkEverywhere(root, name, location, { allowTracked = false, dryRun = f
907
906
  }
908
907
  }
909
908
  if (!dryRun) {
910
- mkdirSync5(join8(root, dir), { recursive: true });
909
+ mkdirSync4(join8(root, dir), { recursive: true });
911
910
  linkDir(join8(root, location, name), link2);
912
911
  }
913
912
  result.created.push(dir);
@@ -915,12 +914,12 @@ function linkEverywhere(root, name, location, { allowTracked = false, dryRun = f
915
914
  return result;
916
915
  }
917
916
  function unlinkEverywhere(root, name, removing) {
918
- const gone = removing ? realpathOrNull2(removing) : null;
917
+ const gone = removing ? realpathOrNull(removing) : null;
919
918
  return ALL_PROJECT_DIRS.filter((dir) => {
920
919
  const link2 = join8(root, dir, name);
921
920
  if (!isLink(link2)) return false;
922
921
  if (dir === GROK_SKILLS_DIR) {
923
- const target = realpathOrNull2(link2);
922
+ const target = realpathOrNull(link2);
924
923
  if (target !== null && target !== gone) return false;
925
924
  }
926
925
  unlinkSync(link2);
@@ -960,6 +959,8 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
960
959
  const to = join8(root, primary, name);
961
960
  const local = isLink(to) && !recorded ? null : treeHash(to);
962
961
  const knownContent = local !== null && (local === recorded?.hash || versionForHash(name, local) !== null);
962
+ if (local !== null && !recorded && local !== target.hash && isGitTracked(root, `${primary}/${name}`))
963
+ return { name, action: "skipped", reason: `${primary}/${name} is the repo's own (committed): skilllib doesn't replace it; remove it in the repo first if the library's should win` };
963
964
  if (local !== null && local !== target.hash && !knownContent && !force) {
964
965
  return {
965
966
  name,
@@ -969,9 +970,9 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
969
970
  }
970
971
  let backedUp;
971
972
  if (local !== target.hash || isLink(to)) {
972
- mkdirSync5(join8(root, primary), { recursive: true });
973
+ mkdirSync4(join8(root, primary), { recursive: true });
973
974
  if (isLink(to)) unlinkSync(to);
974
- else if (local !== null && !knownContent) backedUp = stash(to, "edit-backup");
975
+ else if (local !== null && versionForHash(name, local) === null) backedUp = stash(to, "edit-backup");
975
976
  copySkill(versionDir(name, target.version), to);
976
977
  }
977
978
  const links = [];
@@ -980,7 +981,7 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
980
981
  let shared;
981
982
  for (const dir of linkDirs) {
982
983
  const link2 = join8(root, dir, name);
983
- if (isLink(link2) && realpathOrNull2(link2) === realpathOrNull2(to)) {
984
+ if (isLink(link2) && realpathOrNull(link2) === realpathOrNull(to)) {
984
985
  links.push(dir);
985
986
  continue;
986
987
  }
@@ -995,7 +996,7 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
995
996
  continue;
996
997
  }
997
998
  }
998
- mkdirSync5(join8(root, dir), { recursive: true });
999
+ mkdirSync4(join8(root, dir), { recursive: true });
999
1000
  linkDir(to, link2);
1000
1001
  links.push(dir);
1001
1002
  }
@@ -1022,14 +1023,15 @@ function removeSkill(root, name, { force = false } = {}) {
1022
1023
  if (!recorded) return { name, action: "skipped", reason: "not managed by skilllib in this project" };
1023
1024
  const dir = join8(root, recorded.dir ?? PROJECT_SKILLS_DIR, name);
1024
1025
  const local = treeHash(dir);
1025
- const edited = local !== null && local !== recorded.hash && !versionForHash(name, local);
1026
+ const inLibrary = local !== null && versionForHash(name, local) !== null;
1027
+ const edited = local !== null && local !== recorded.hash && !inLibrary;
1026
1028
  if (edited && !force) return { name, action: "skipped", reason: "has local edits (use --force to delete anyway)" };
1027
1029
  for (const linkDir2 of recorded.links ?? []) {
1028
1030
  const link2 = join8(root, linkDir2, name);
1029
1031
  if (isLink(link2)) unlinkSync(link2);
1030
1032
  }
1031
- const backedUp = edited ? stash(dir, "edit-backup") : void 0;
1032
- if (!backedUp) rmSync2(dir, { recursive: true, force: true });
1033
+ const backedUp = local !== null && !inLibrary ? stash(dir, "edit-backup") : void 0;
1034
+ if (!backedUp) rmSync3(dir, { recursive: true, force: true });
1033
1035
  delete manifest.skills[name];
1034
1036
  writeManifest(root, manifest);
1035
1037
  return { name, action: "removed", ...backedUp ? { backedUp } : {} };
@@ -1040,23 +1042,41 @@ function syncProject(root, { force = false } = {}) {
1040
1042
  function syncSkill(root, name, { force = false } = {}) {
1041
1043
  const dep = readManifest(root).skills[name];
1042
1044
  if (!dep) return [];
1043
- const local = treeHash(join8(root, dep.dir ?? PROJECT_SKILLS_DIR, name));
1044
- const linksOk = (dep.links ?? []).every((d) => existsSync7(join8(root, d, name, "SKILL.md")));
1045
+ const copy = join8(root, dep.dir ?? PROJECT_SKILLS_DIR, name);
1046
+ const local = treeHash(copy);
1047
+ const linksOk = (dep.links ?? []).every((d) => realpathOrNull(join8(root, d, name)) === realpathOrNull(copy));
1045
1048
  if (local === dep.hash && linksOk) return [];
1049
+ if (local === dep.hash) return [relinkPinned(root, name, dep)];
1046
1050
  if (local !== null && local !== dep.hash && !force) return [{ name, action: "skipped", reason: "has local edits (use --force to reset them)" }];
1047
- const change = addSkill(root, name, { force: true, version: pinnedVersion(name, dep) });
1048
- 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` }];
1051
+ const version = pinnedVersion(name, dep);
1052
+ if (typeof version === "string") return [{ name, action: "skipped", reason: version }];
1053
+ const change = addSkill(root, name, { force: true, version });
1049
1054
  return [{ ...change, action: local === null ? "installed" : change.action }];
1050
1055
  }
1056
+ function relinkPinned(root, name, dep) {
1057
+ const to = join8(root, dep.dir ?? PROJECT_SKILLS_DIR, name);
1058
+ for (const dir of dep.links ?? []) {
1059
+ const link2 = join8(root, dir, name);
1060
+ if (realpathOrNull(link2) === realpathOrNull(to)) continue;
1061
+ if (isLink(link2)) unlinkSync(link2);
1062
+ else if (entryExists(link2)) return { name, action: "skipped", reason: `${dir}/${name} is in the way of its link: move it, then sync` };
1063
+ mkdirSync4(join8(root, dir), { recursive: true });
1064
+ linkDir(to, link2);
1065
+ }
1066
+ return { name, action: "relinked", to: dep.version };
1067
+ }
1051
1068
  function pinnedVersion(name, dep) {
1052
- return versionForHash(name, dep.hash)?.version ?? (dep.version > 0 && getVersion(name, dep.version) ? dep.version : void 0);
1069
+ if (!latestVersion(name)) return `not in your library (${tildify(libraryDir())}); import it, or set SKILLLIB_HOME to the library that has it`;
1070
+ return versionForHash(name, dep.hash)?.version ?? `your library has no copy of the version skilllib.json pins (pinned from another library?); skilllib update ${name} installs your newest`;
1053
1071
  }
1054
1072
  function discardEdits(root, name) {
1055
1073
  const dep = readManifest(root).skills[name];
1056
1074
  if (!dep) return { name, action: "skipped", reason: "not managed by skilllib in this project" };
1075
+ const version = pinnedVersion(name, dep);
1076
+ if (typeof version === "string") return { name, action: "skipped", reason: version };
1057
1077
  const dir = join8(root, dep.dir ?? PROJECT_SKILLS_DIR, name);
1058
1078
  if (existsSync7(dir) && !isLink(dir)) stash(dir, "edit-backup");
1059
- return addSkill(root, name, { force: true, version: pinnedVersion(name, dep) });
1079
+ return addSkill(root, name, { force: true, version });
1060
1080
  }
1061
1081
  function updateProject(root, names, { force = false } = {}) {
1062
1082
  const { skills } = readManifest(root);
@@ -1078,7 +1098,7 @@ function importSkill(dir, { force = false } = {}) {
1078
1098
  const incoming = treeHash(dir);
1079
1099
  if (existing === incoming) return { name, status: "unchanged" };
1080
1100
  if (existing !== null && !force) return { name, status: "exists" };
1081
- mkdirSync5(libraryDir(), { recursive: true });
1101
+ mkdirSync4(libraryDir(), { recursive: true });
1082
1102
  const origin = existing !== null && libraryOrigins()[name] ? null : originFor(dir);
1083
1103
  copySkill(dir, to);
1084
1104
  if (origin) recordOrigin(name, origin);
@@ -1090,19 +1110,15 @@ function backupIndexFile(bucket) {
1090
1110
  return join8(skilllibHome(), bucket, "index.json");
1091
1111
  }
1092
1112
  function readBackupIndex(bucket) {
1093
- try {
1094
- return JSON.parse(readFileSync6(backupIndexFile(bucket), "utf-8"));
1095
- } catch {
1096
- return {};
1097
- }
1113
+ return readState(backupIndexFile(bucket)) ?? {};
1098
1114
  }
1099
1115
  function move(from, to) {
1100
1116
  try {
1101
- renameSync(from, to);
1117
+ renameSync2(from, to);
1102
1118
  } catch (err) {
1103
1119
  if (err.code !== "EXDEV") throw err;
1104
1120
  cpSync2(from, to, { recursive: true, verbatimSymlinks: true });
1105
- rmSync2(from, { recursive: true, force: true });
1121
+ rmSync3(from, { recursive: true, force: true });
1106
1122
  }
1107
1123
  }
1108
1124
  function stash(dir, bucket) {
@@ -1111,9 +1127,10 @@ function stash(dir, bucket) {
1111
1127
  while (entryExists(join8(skilllibHome(), bucket, entryAt(at)))) at++;
1112
1128
  const entry = entryAt(at);
1113
1129
  const target = join8(skilllibHome(), bucket, entry);
1114
- mkdirSync5(join8(skilllibHome(), bucket), { recursive: true });
1130
+ const index = readBackupIndex(bucket);
1131
+ mkdirSync4(join8(skilllibHome(), bucket), { recursive: true });
1115
1132
  move(dir, target);
1116
- writeFileSync5(backupIndexFile(bucket), JSON.stringify({ ...readBackupIndex(bucket), [entry]: dir }, null, 2) + "\n");
1133
+ writeState(backupIndexFile(bucket), { ...index, [entry]: dir });
1117
1134
  return target;
1118
1135
  }
1119
1136
  function removeUntracked(root, name, path) {
@@ -1121,7 +1138,7 @@ function removeUntracked(root, name, path) {
1121
1138
  return stash(path, "trash");
1122
1139
  }
1123
1140
  function projectsUsing(name) {
1124
- return knownProjects().filter((p) => name in readManifest(p).skills);
1141
+ return knownProjects().filter((p) => name in pinsIn(p));
1125
1142
  }
1126
1143
  function deleteLibrarySkill(name) {
1127
1144
  const using = projectsUsing(name);
@@ -1150,12 +1167,12 @@ function linkGlobal(path, agents) {
1150
1167
  const skipped = [];
1151
1168
  for (const dir of new Set(agents.map((id) => harness(id).globalDirs()[0]))) {
1152
1169
  const link2 = join8(dir, name);
1153
- if (isLink(link2) && realpathOrNull2(link2) === null) unlinkSync(link2);
1170
+ if (isLink(link2) && realpathOrNull(link2) === null) unlinkSync(link2);
1154
1171
  else if (entryExists(link2)) {
1155
- if (realpathOrNull2(link2) !== realpathOrNull2(path)) skipped.push(link2);
1172
+ if (realpathOrNull(link2) !== realpathOrNull(path)) skipped.push(link2);
1156
1173
  continue;
1157
1174
  }
1158
- mkdirSync5(dir, { recursive: true });
1175
+ mkdirSync4(dir, { recursive: true });
1159
1176
  linkDir(path, link2);
1160
1177
  linked.push(link2);
1161
1178
  }
@@ -1165,8 +1182,8 @@ function createSkill(name, description) {
1165
1182
  if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) return { ok: false, reason: "use lowercase letters, digits and dashes" };
1166
1183
  const dir = librarySkillDir(name);
1167
1184
  if (existsSync7(dir)) return { ok: false, reason: `${name} already exists` };
1168
- mkdirSync5(dir, { recursive: true });
1169
- writeFileSync5(
1185
+ mkdirSync4(dir, { recursive: true });
1186
+ writeFileSync2(
1170
1187
  join8(dir, "SKILL.md"),
1171
1188
  `---
1172
1189
  name: ${name}
@@ -1190,26 +1207,28 @@ function listBackups() {
1190
1207
  const match = entry.match(/^(.*)-(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-\d{3}Z$/);
1191
1208
  if (!match) return [];
1192
1209
  const name = match[1];
1193
- const from = index[entry] ?? (kind === "trash" ? librarySkillDir(name) : join8(claudeDir(), "skills", name));
1194
- return [{ name, kind, path: join8(dir, entry), movedAt: `${match[2]} ${match[3]}:${match[4]}`, from }];
1210
+ const from = index[entry] ?? (kind === "trash" ? librarySkillDir(name) : kind === "global-backup" ? join8(claudeDir(), "skills", name) : "");
1211
+ return [{ name, kind, path: join8(dir, entry), movedAt: localStamp(Date.parse(`${match[2]}T${match[3]}:${match[4]}:${match[5]}Z`)), from }];
1195
1212
  });
1196
1213
  }).sort((a, b) => b.path.slice(-24).localeCompare(a.path.slice(-24)));
1197
1214
  }
1198
1215
  function backupFrom(backup) {
1199
1216
  if (backup.kind === "plugin") return "Claude Code plugin";
1200
- return backup.from === librarySkillDir(backup.name) ? "library (deleted)" : tildify(dirname2(backup.from));
1217
+ if (!backup.from) return "unknown";
1218
+ return backup.from === librarySkillDir(backup.name) ? "library (deleted)" : tildify(dirname3(backup.from));
1201
1219
  }
1202
1220
  function restoreBackup(backup) {
1203
1221
  const to = backup.from;
1222
+ if (!to) return { ok: false, reason: `skilllib doesn't know where it came from; its files are in ${tildify(backup.path)}` };
1204
1223
  if (backup.kind === "tidy-backup" && isLink(to)) unlinkSync(to);
1205
- if (backup.kind === "edit-backup" && !isLink(to) && versionForHash(backup.name, treeHash(to) ?? "") !== null) rmSync2(to, { recursive: true, force: true });
1224
+ if (backup.kind === "edit-backup" && !isLink(to) && versionForHash(backup.name, treeHash(to) ?? "") !== null) rmSync3(to, { recursive: true, force: true });
1206
1225
  let taken = existsSync7(to);
1207
1226
  try {
1208
1227
  taken = taken || lstatSync2(to) !== null;
1209
1228
  } catch {
1210
1229
  }
1211
1230
  if (taken) return { ok: false, reason: `${backup.name} already exists there` };
1212
- mkdirSync5(join8(to, ".."), { recursive: true });
1231
+ mkdirSync4(join8(to, ".."), { recursive: true });
1213
1232
  move(backup.path, to);
1214
1233
  forgetLatest(backup.name);
1215
1234
  return { ok: true, to };
@@ -1218,7 +1237,7 @@ function restoreBackup(backup) {
1218
1237
  // src/agentSkill.ts
1219
1238
  var AGENT_SKILL_NAME = "skilllib";
1220
1239
  function shippedSkill() {
1221
- return readFileSync7(new URL("../skills/skilllib/SKILL.md", import.meta.url), "utf-8");
1240
+ return readFileSync4(new URL("../skills/skilllib/SKILL.md", import.meta.url), "utf-8");
1222
1241
  }
1223
1242
  function agentSkillDirs(enabled = enabledHarnesses()) {
1224
1243
  const claudeSkills = join9(claudeDir(), "skills");
@@ -1244,7 +1263,7 @@ function entryExists2(path) {
1244
1263
  function agentSkillState(enabled = enabledHarnesses()) {
1245
1264
  const [primary, ...links] = agentSkillDirs(enabled);
1246
1265
  if (!primary || !isOurs(primary) || links.some((l) => !existsSync8(join9(l, "SKILL.md")))) return "missing";
1247
- return readFileSync7(join9(primary, "SKILL.md"), "utf-8") === shippedSkill() ? "installed" : "outdated";
1266
+ return readFileSync4(join9(primary, "SKILL.md"), "utf-8") === shippedSkill() ? "installed" : "outdated";
1248
1267
  }
1249
1268
  function installAgentSkill(enabled = enabledHarnesses()) {
1250
1269
  const [primary, ...links] = agentSkillDirs(enabled);
@@ -1252,12 +1271,12 @@ function installAgentSkill(enabled = enabledHarnesses()) {
1252
1271
  const foreign = [primary, ...links].find((d) => entryExists2(d) && !isOurs(d) && !isDangling(d));
1253
1272
  if (foreign) return { ok: false, reason: `${tildify(foreign)} already exists and isn't skilllib's` };
1254
1273
  for (const d of [primary, ...links]) if (isDangling(d)) unlinkSync2(d);
1255
- mkdirSync6(primary, { recursive: true });
1256
- writeFileSync6(join9(primary, "SKILL.md"), shippedSkill());
1257
- writeFileSync6(join9(primary, OWN_SKILL_MARKER), "Installed by skilllib. `skilllib agent-skill remove` removes it.\n");
1274
+ mkdirSync5(primary, { recursive: true });
1275
+ writeFileSync3(join9(primary, "SKILL.md"), shippedSkill());
1276
+ writeFileSync3(join9(primary, OWN_SKILL_MARKER), "Installed by skilllib. `skilllib agent-skill remove` removes it.\n");
1258
1277
  for (const link2 of links) {
1259
1278
  if (entryExists2(link2)) continue;
1260
- mkdirSync6(join9(link2, ".."), { recursive: true });
1279
+ mkdirSync5(join9(link2, ".."), { recursive: true });
1261
1280
  linkDir(primary, link2);
1262
1281
  }
1263
1282
  return { ok: true, dirs: [primary, ...links] };
@@ -1266,8 +1285,9 @@ function removeAgentSkill() {
1266
1285
  const all = agentSkillDirs(["claude-code", "codex"]);
1267
1286
  const removed = all.filter((d) => entryExists2(d) && (isOurs(d) || isDangling(d)));
1268
1287
  for (const d of removed) {
1288
+ if (!entryExists2(d)) continue;
1269
1289
  if (lstatSync3(d).isSymbolicLink()) unlinkSync2(d);
1270
- else rmSync3(d, { recursive: true, force: true });
1290
+ else rmSync4(d, { recursive: true, force: true });
1271
1291
  }
1272
1292
  return removed;
1273
1293
  }
@@ -1279,9 +1299,9 @@ function refreshAgentSkill() {
1279
1299
  }
1280
1300
 
1281
1301
  // src/commands.ts
1282
- import { existsSync as existsSync13, readFileSync as readFileSync9 } from "fs";
1302
+ import { existsSync as existsSync14, readFileSync as readFileSync6 } from "fs";
1283
1303
  import { parseArgs as parseFlags } from "util";
1284
- import { basename as basename9, join as join14, resolve as resolve7, sep as sep5 } from "path";
1304
+ import { basename as basename9, join as join15, resolve as resolve7, sep as sep6 } from "path";
1285
1305
 
1286
1306
  // src/git.ts
1287
1307
  import { execFileSync as execFileSync2 } from "child_process";
@@ -1323,24 +1343,17 @@ function relativeTo(root, path) {
1323
1343
  }
1324
1344
 
1325
1345
  // src/here.ts
1326
- import { readdirSync as readdirSync7 } from "fs";
1346
+ import { readdirSync as readdirSync8 } from "fs";
1327
1347
  import { basename as basename7 } from "path";
1328
1348
 
1329
1349
  // src/health.ts
1330
- import { lstatSync as lstatSync4 } from "fs";
1331
- import { basename as basename6, dirname as dirname5 } from "path";
1350
+ import { existsSync as existsSync12, lstatSync as lstatSync4, readdirSync as readdirSync7, unlinkSync as unlinkSync4 } from "fs";
1351
+ import { basename as basename6, dirname as dirname6, join as join13 } from "path";
1332
1352
 
1333
1353
  // src/plugins.ts
1334
1354
  import { execFileSync as execFileSync3 } from "child_process";
1335
- import { existsSync as existsSync10, mkdirSync as mkdirSync7, readdirSync as readdirSync5, readFileSync as readFileSync8, statSync as statSync3, writeFileSync as writeFileSync7 } from "fs";
1336
- import { basename as basename4, dirname as dirname3, join as join11, resolve as resolve6 } from "path";
1337
- function readJson2(path) {
1338
- try {
1339
- return JSON.parse(readFileSync8(path, "utf-8"));
1340
- } catch {
1341
- return null;
1342
- }
1343
- }
1355
+ import { existsSync as existsSync10, mkdirSync as mkdirSync6, readdirSync as readdirSync5, readFileSync as readFileSync5, statSync as statSync4, writeFileSync as writeFileSync4 } from "fs";
1356
+ import { basename as basename4, dirname as dirname4, join as join11, resolve as resolve6 } from "path";
1344
1357
  function nonEmptyDir(path) {
1345
1358
  try {
1346
1359
  return readdirSync5(path).some((f) => !f.startsWith("."));
@@ -1349,7 +1362,7 @@ function nonEmptyDir(path) {
1349
1362
  }
1350
1363
  }
1351
1364
  function extrasOf(root) {
1352
- const manifest = readJson2(join11(root, ".claude-plugin", "plugin.json")) ?? {};
1365
+ const manifest = readJson(join11(root, ".claude-plugin", "plugin.json")) ?? {};
1353
1366
  return [
1354
1367
  ...existsSync10(join11(root, ".mcp.json")) || manifest.mcpServers ? ["MCP servers"] : [],
1355
1368
  ...existsSync10(join11(root, "hooks", "hooks.json")) || manifest.hooks ? ["hooks"] : [],
@@ -1358,21 +1371,20 @@ function extrasOf(root) {
1358
1371
  ];
1359
1372
  }
1360
1373
  function claudePlugins(machine, root) {
1361
- const installed = readJson2(join11(claudeDir(), "plugins", "installed_plugins.json"))?.plugins ?? {};
1374
+ const installed = installedPluginsJson();
1362
1375
  const byOrigin = /* @__PURE__ */ new Map();
1363
1376
  for (const m of machine) if (m.kind === "plugin") byOrigin.set(m.origin, [...byOrigin.get(m.origin) ?? [], m.path]);
1364
1377
  return [...byOrigin].map(([origin, skills]) => {
1365
1378
  const synced = origin.endsWith("@synced");
1366
- const id = origin;
1367
1379
  const entries = installed[origin] ?? [];
1368
1380
  const entry = entries.find((e) => root && e.projectPath === root) ?? entries.find((e) => (e.scope ?? "user") === "user") ?? entries[0];
1369
1381
  return {
1370
- id,
1382
+ id: origin,
1371
1383
  scope: synced ? null : entry?.scope ?? "user",
1372
1384
  ...entry?.projectPath ? { projectPath: entry.projectPath } : {},
1373
1385
  synced,
1374
1386
  skills,
1375
- extras: extrasOf(dirname3(dirname3(skills[0])))
1387
+ extras: extrasOf(dirname4(dirname4(skills[0])))
1376
1388
  };
1377
1389
  });
1378
1390
  }
@@ -1385,10 +1397,10 @@ function newer(a, b) {
1385
1397
  }
1386
1398
  function updateOf(name, marketplace, entry, catalogs) {
1387
1399
  const dir = join11(claudeDir(), "plugins", "marketplaces", marketplace);
1388
- if (!catalogs.has(marketplace)) catalogs.set(marketplace, readJson2(join11(dir, ".claude-plugin", "marketplace.json"))?.plugins ?? []);
1400
+ if (!catalogs.has(marketplace)) catalogs.set(marketplace, readJson(join11(dir, ".claude-plugin", "marketplace.json"))?.plugins ?? []);
1389
1401
  const listed = catalogs.get(marketplace).find((p) => p.name === name);
1390
1402
  if (!listed) return void 0;
1391
- const version = listed.version ?? (typeof listed.source === "string" ? readJson2(join11(dir, listed.source, ".claude-plugin", "plugin.json"))?.version : void 0);
1403
+ const version = listed.version ?? (typeof listed.source === "string" ? readJson(join11(dir, listed.source, ".claude-plugin", "plugin.json"))?.version : void 0);
1392
1404
  if (version && entry.version) return newer(version, entry.version) ? version : void 0;
1393
1405
  const sha = typeof listed.source === "object" ? listed.source?.sha : void 0;
1394
1406
  return sha && entry.gitCommitSha && sha !== entry.gitCommitSha ? "newer" : void 0;
@@ -1398,12 +1410,12 @@ function pluginRoot(id, entry) {
1398
1410
  return candidates.find((c) => existsSync10(join11(c, "skills"))) ?? candidates.find((c) => existsSync10(c)) ?? candidates[0];
1399
1411
  }
1400
1412
  function describe(root) {
1401
- const manifest = readJson2(join11(root, ".claude-plugin", "plugin.json")) ?? {};
1413
+ const manifest = readJson(join11(root, ".claude-plugin", "plugin.json")) ?? {};
1402
1414
  return { description: manifest.description ?? "", version: manifest.version, skills: skillDirsIn(join11(root, "skills")), extras: extrasOf(root) };
1403
1415
  }
1404
1416
  function installedPlugins() {
1405
1417
  const plugins = join11(claudeDir(), "plugins");
1406
- const installed = readJson2(join11(plugins, "installed_plugins.json"))?.plugins ?? {};
1418
+ const installed = installedPluginsJson();
1407
1419
  const catalogs = /* @__PURE__ */ new Map();
1408
1420
  const fromMarketplaces = Object.entries(installed).flatMap(
1409
1421
  ([id, entries]) => entries.map((entry) => {
@@ -1435,18 +1447,18 @@ function installedPlugins() {
1435
1447
  const syncedDir = join11(plugins, "synced");
1436
1448
  const dirs = (p) => existsSync10(p) ? readdirSync5(p, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".")).map((d) => join11(p, d.name)) : [];
1437
1449
  const fromSynced = dirs(syncedDir).flatMap(dirs).map((root) => {
1438
- const id = `${readJson2(join11(root, ".claude-plugin", "plugin.json"))?.name ?? basename4(root)}@synced`;
1450
+ const id = `${readJson(join11(root, ".claude-plugin", "plugin.json"))?.name ?? basename4(root)}@synced`;
1439
1451
  return { id, scope: null, synced: true, agent: "claude-code", on: user[id] !== false, root, ...describe(root) };
1440
1452
  });
1441
1453
  return [...fromMarketplaces, ...fromSettings, ...fromSynced];
1442
1454
  }
1443
1455
  function cursorPlugins() {
1444
1456
  const cache = join11(userHome(), ".cursor", "plugins", "cache");
1445
- const dirs = (p) => existsSync10(p) ? readdirSync5(p).filter((d) => !d.startsWith(".") && statSync3(join11(p, d)).isDirectory()) : [];
1457
+ const dirs = (p) => existsSync10(p) ? readdirSync5(p).filter((d) => !d.startsWith(".") && statSync4(join11(p, d)).isDirectory()) : [];
1446
1458
  return dirs(cache).flatMap(
1447
1459
  (marketplace) => dirs(join11(cache, marketplace)).flatMap((plugin) => {
1448
1460
  const versions = dirs(join11(cache, marketplace, plugin)).map((v) => join11(cache, marketplace, plugin, v));
1449
- const root = versions.sort((a, b) => statSync3(b).mtimeMs - statSync3(a).mtimeMs)[0];
1461
+ const root = versions.sort((a, b) => statSync4(b).mtimeMs - statSync4(a).mtimeMs)[0];
1450
1462
  if (!root) return [];
1451
1463
  const about = describe(root);
1452
1464
  return [{ id: `${plugin}@${marketplace}`, scope: null, synced: false, agent: "cursor", on: null, root, ...about, version: about.version ?? basename4(root) }];
@@ -1485,11 +1497,10 @@ function claude(args, cwd, bin = claudeBinary()) {
1485
1497
  }
1486
1498
  }
1487
1499
  function setPluginIn(file, id, on) {
1488
- const settings = existsSync10(file) ? readJson2(file) : {};
1500
+ const settings = existsSync10(file) ? readJson(file) : {};
1489
1501
  if (!settings) return { ok: false, reason: `can't read ${file}` };
1490
1502
  const enabled = settings.enabledPlugins ?? {};
1491
- mkdirSync7(dirname3(file), { recursive: true });
1492
- writeFileSync7(file, JSON.stringify({ ...settings, enabledPlugins: { ...enabled, [id]: on } }, null, 2) + "\n");
1503
+ writeState(file, { ...settings, enabledPlugins: { ...enabled, [id]: on } });
1493
1504
  return { ok: true };
1494
1505
  }
1495
1506
  function ignoreLocalSettings(root) {
@@ -1512,9 +1523,9 @@ function ignoreLocalSettings(root) {
1512
1523
  }
1513
1524
  try {
1514
1525
  const exclude = resolve6(root, git2("rev-parse", "--git-path", "info/exclude"));
1515
- const before = existsSync10(exclude) ? readFileSync8(exclude, "utf-8") : "";
1516
- mkdirSync7(dirname3(exclude), { recursive: true });
1517
- writeFileSync7(exclude, `${before}${before && !before.endsWith("\n") ? "\n" : ""}/${git2("rev-parse", "--show-prefix")}${file}
1526
+ const before = existsSync10(exclude) ? readFileSync5(exclude, "utf-8") : "";
1527
+ mkdirSync6(dirname4(exclude), { recursive: true });
1528
+ writeFileSync4(exclude, `${before}${before && !before.endsWith("\n") ? "\n" : ""}/${git2("rev-parse", "--show-prefix")}${file}
1518
1529
  `);
1519
1530
  return ", which git now ignores via .git/info/exclude";
1520
1531
  } catch {
@@ -1582,32 +1593,38 @@ function removePlugin(plugin, how) {
1582
1593
  if (plugin.scope && plugin.scope !== "user" && !plugin.projectPath) {
1583
1594
  return { ok: false, message: `${plugin.id}: installed for one project; run \`claude plugin ${how === "delete" ? "uninstall" : "disable"} ${plugin.id} --scope ${plugin.scope}\` there` };
1584
1595
  }
1585
- if (!plugin.synced && !claudeBinary()) {
1596
+ const viaSettings = plugin.synced || !claudeBinary() && how === "off" && !plugin.projectPath;
1597
+ if (!viaSettings && !claudeBinary()) {
1586
1598
  return { ok: false, message: `${plugin.id}: nothing changed; Claude Code's CLI didn't run (tried \`claude\` on your PATH and ~/.local/bin/claude)` };
1587
1599
  }
1588
- const imported = plugin.skills.filter((dir) => latestVersion(basename4(dir)) === null).map((dir) => importSkill(dir).name);
1600
+ const wasOn = enabledPlugins(plugin.projectPath)[plugin.id] === true;
1601
+ const yours = new Set(machineSkills().filter((m) => m.movable).map((m) => m.name));
1602
+ const imported = plugin.skills.filter((dir) => !yours.has(basename4(dir)) && latestVersion(basename4(dir)) === null).map((dir) => importSkill(dir).name);
1589
1603
  const scope = plugin.scope ? ["--scope", plugin.scope] : [];
1590
- const r = plugin.synced ? (
1604
+ const r = viaSettings ? (
1591
1605
  // Synced plugins aren't installed, so `claude plugin` can't touch them: their switch is enabledPlugins.
1592
1606
  setPluginIn(join11(claudeDir(), "settings.json"), plugin.id, false)
1593
- ) : claude(["plugin", how === "delete" ? "uninstall" : "disable", plugin.id, ...scope], plugin.projectPath);
1607
+ ) : (
1608
+ // --keep-data, as uninstallPlugin: its saved data stays, so a restore brings it back as it was.
1609
+ claude(["plugin", how === "delete" ? "uninstall" : "disable", plugin.id, ...how === "delete" ? ["--keep-data"] : [], ...scope], plugin.projectPath)
1610
+ );
1594
1611
  const saved = imported.length ? `; ${imported.join(", ")} copied into Your skills` : "";
1595
1612
  if (!r.ok) return { ok: false, message: `${plugin.id}: ${r.reason}${saved}` };
1596
- if (how === "delete" && !plugin.synced) recordRemovedPlugin(plugin.id, plugin.scope ?? "user", plugin.projectPath, true);
1613
+ if (how === "delete" && !plugin.synced) recordRemovedPlugin(plugin.id, plugin.scope ?? "user", plugin.projectPath, wasOn);
1597
1614
  return { ok: true, message: `${plugin.id} ${how === "delete" && !plugin.synced ? "removed" : "turned off"}${saved}` };
1598
1615
  }
1599
1616
  function recordRemovedPlugin(id, scope, projectPath, on = true) {
1600
- const list2 = readJson2(removedFile()) ?? [];
1601
- mkdirSync7(skilllibHome(), { recursive: true });
1602
- writeFileSync7(removedFile(), JSON.stringify([...list2, { id, scope, ...projectPath ? { projectPath } : {}, on, at: (/* @__PURE__ */ new Date()).toISOString() }], null, 2) + "\n");
1617
+ const list2 = readState(removedFile()) ?? [];
1618
+ mkdirSync6(skilllibHome(), { recursive: true });
1619
+ writeState(removedFile(), [...list2, { id, scope, ...projectPath ? { projectPath } : {}, on, at: (/* @__PURE__ */ new Date()).toISOString() }]);
1603
1620
  }
1604
1621
  function pluginBackups() {
1605
- return (readJson2(removedFile()) ?? []).map((r) => ({
1622
+ return (readState(removedFile()) ?? []).map((r) => ({
1606
1623
  name: r.id,
1607
1624
  kind: "plugin",
1608
1625
  // Project and local installs go back into their project.
1609
1626
  path: r.projectPath ? `${r.scope}:${r.projectPath}` : r.scope,
1610
- movedAt: r.at.slice(0, 16).replace("T", " "),
1627
+ movedAt: localStamp(Date.parse(r.at)),
1611
1628
  from: r.id
1612
1629
  }));
1613
1630
  }
@@ -1616,11 +1633,11 @@ function restorePlugin(backup) {
1616
1633
  const projectPath = rest.join(":") || void 0;
1617
1634
  const r = claude(["plugin", "install", backup.from, "--scope", scope], projectPath);
1618
1635
  if (!r.ok) return r;
1619
- const records = readJson2(removedFile()) ?? [];
1636
+ const records = readState(removedFile()) ?? [];
1620
1637
  const same = (x) => x.id === backup.from && (x.projectPath ?? void 0) === projectPath && x.scope === scope;
1621
1638
  const wasOff = records.filter(same).pop()?.on === false;
1622
1639
  const off = wasOff ? claude(["plugin", "disable", backup.from, "--scope", scope], projectPath) : null;
1623
- writeFileSync7(removedFile(), JSON.stringify(records.filter((x) => !same(x)), null, 2) + "\n");
1640
+ writeState(removedFile(), records.filter((x) => !same(x)));
1624
1641
  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" : ""})`;
1625
1642
  return { ok: true, to: where };
1626
1643
  }
@@ -1629,14 +1646,14 @@ function cursorPluginSkills(plugins = cursorPlugins()) {
1629
1646
  }
1630
1647
 
1631
1648
  // src/tidy.ts
1632
- import { existsSync as existsSync11, mkdirSync as mkdirSync8, readdirSync as readdirSync6, unlinkSync as unlinkSync3 } from "fs";
1633
- import { basename as basename5, dirname as dirname4, join as join12, sep as sep4 } from "path";
1649
+ import { existsSync as existsSync11, mkdirSync as mkdirSync7, readdirSync as readdirSync6, unlinkSync as unlinkSync3 } from "fs";
1650
+ import { basename as basename5, dirname as dirname5, join as join12, sep as sep5 } from "path";
1634
1651
  function entriesOf(dirs, name) {
1635
1652
  return dirs.flatMap((dir) => {
1636
1653
  const path = join12(dir, name);
1637
1654
  if (!entryExists(path)) return [];
1638
1655
  const link2 = isLink(path);
1639
- const real = realpathOrNull2(path);
1656
+ const real = realpathOrNull(path);
1640
1657
  const skill = existsSync11(join12(path, "SKILL.md"));
1641
1658
  if (!link2 && !skill) return [];
1642
1659
  return [{ dir, path, link: link2, real, hash: null }];
@@ -1661,7 +1678,7 @@ function globalTidyDirs() {
1661
1678
  function runsFrom(id, dirsWithCopy, readsOf) {
1662
1679
  const reads = readsOf(id).filter((d) => dirsWithCopy.includes(d));
1663
1680
  if (id === "codex") return reads;
1664
- const pick = (d) => CURSOR_PICKS.indexOf(basename5(dirname4(d)));
1681
+ const pick = (d) => CURSOR_PICKS.indexOf(basename5(dirname5(d)));
1665
1682
  return id === "cursor" ? [...reads].sort((a, b) => pick(a) - pick(b)).slice(0, 1) : reads.slice(0, 1);
1666
1683
  }
1667
1684
  function conflictOf(name, root, reals, enabled, readsOf) {
@@ -1677,17 +1694,17 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
1677
1694
  const { skills } = readManifest(root);
1678
1695
  const abs = (dir) => join12(root, dir);
1679
1696
  const rel = (dir) => relativeTo(root, dir);
1680
- const readsOf = (id) => HARNESSES.find((h) => h.id === id).projectDirs.map(abs);
1697
+ const readsOf = (id) => harness(id).projectDirs.map(abs);
1681
1698
  let git2;
1682
1699
  const skillsLock = projectSkillsLock(root);
1683
1700
  for (const name of namesIn(PROJECT_TIDY_DIRS.map(abs))) {
1684
1701
  const entries = entriesOf(PROJECT_TIDY_DIRS.map(abs), name);
1685
- const external = entries.find((e) => e.link && e.real && !(e.real + sep4).startsWith(realpathOrNull2(root) + sep4));
1702
+ const external = entries.find((e) => e.link && e.real && !(e.real + sep5).startsWith(realpathOrNull(root) + sep5));
1686
1703
  if (external) {
1687
1704
  report.skipped.push({ name, reason: `${rel(external.path)} links outside the repo` });
1688
1705
  continue;
1689
1706
  }
1690
- const reals = entries.filter((e) => !e.link);
1707
+ const reals = entries.filter((e, i, all) => !e.link && all.findIndex((o) => !o.link && realpathOrNull(o.path) === realpathOrNull(e.path)) === i);
1691
1708
  if (reals.length === 0) continue;
1692
1709
  hashReals(reals);
1693
1710
  const dep = skills[name];
@@ -1713,10 +1730,11 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
1713
1730
  const wanted = new Set([...installDirs(enabled, rel(primary.dir)), ...dep?.links ?? []].map(abs));
1714
1731
  const steps = [];
1715
1732
  for (const e of entries) {
1716
- if (e === primary) continue;
1733
+ if (e === primary || !e.link && e.real === primary.real) continue;
1717
1734
  const needed = wanted.has(e.dir);
1718
1735
  if (e.link) {
1719
1736
  if (e.real === primary.real) {
1737
+ if (!needed && !dep && ["committed", "changed"].includes((git2 ??= gitInfo(root))?.of(rel(e.path)) ?? "")) continue;
1720
1738
  if (!needed) steps.push({ kind: "unlink", path: e.path });
1721
1739
  } else steps.push(needed ? { kind: "link", path: e.path, target: primary.path } : { kind: "unlink", path: e.path });
1722
1740
  } else {
@@ -1729,7 +1747,7 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
1729
1747
  return report;
1730
1748
  }
1731
1749
  function globalReadsOf(id) {
1732
- return HARNESSES.find((h) => h.id === id).globalDirs().map(globalDirAs);
1750
+ return harness(id).globalDirs().map(globalDirAs);
1733
1751
  }
1734
1752
  function isSkillsDirPlugin(path) {
1735
1753
  return existsSync11(join12(path, ".claude-plugin", "plugin.json"));
@@ -1744,7 +1762,7 @@ function planGlobalTidy({ keep = {}, enabled = enabledHarnesses() } = {}) {
1744
1762
  report.skipped.push({ name, reason: "a Claude Code plugin kept in a skills folder" });
1745
1763
  continue;
1746
1764
  }
1747
- const reals = entries.filter((e, i, all) => !e.link && all.findIndex((o) => !o.link && realpathOrNull2(o.path) === realpathOrNull2(e.path)) === i);
1765
+ const reals = entries.filter((e, i, all) => !e.link && all.findIndex((o) => !o.link && realpathOrNull(o.path) === realpathOrNull(e.path)) === i);
1748
1766
  if (reals.length < 2) continue;
1749
1767
  hashReals(reals);
1750
1768
  const kept = reals.find((r) => r.dir === keep[name]);
@@ -1770,11 +1788,12 @@ function applyTidy(plan, { git: git2 = "keep", info: info2 } = {}) {
1770
1788
  const applied = [];
1771
1789
  for (const step of plan.steps) {
1772
1790
  if (held.includes(step) || !entryExists(step.path) || "target" in step && !entryExists(step.target)) continue;
1791
+ if ("target" in step && realpathOrNull(step.path) === realpathOrNull(step.target)) continue;
1773
1792
  if (step.kind === "link" || step.kind === "unlink") {
1774
1793
  if (isLink(step.path)) unlinkSync3(step.path);
1775
1794
  } else stash(step.path, "tidy-backup");
1776
1795
  if (step.kind === "link" || step.kind === "replace") {
1777
- mkdirSync8(dirname4(step.path), { recursive: true });
1796
+ mkdirSync7(dirname5(step.path), { recursive: true });
1778
1797
  linkDir(step.target, step.path);
1779
1798
  }
1780
1799
  applied.push(step);
@@ -1782,7 +1801,7 @@ function applyTidy(plan, { git: git2 = "keep", info: info2 } = {}) {
1782
1801
  if (plan.root) {
1783
1802
  const manifest = readManifest(plan.root);
1784
1803
  const dep = manifest.skills[plan.name];
1785
- const added = applied.filter((s) => s.kind === "link" || s.kind === "replace").map((s) => relativeTo(plan.root, dirname4(s.path)));
1804
+ const added = applied.filter((s) => s.kind === "link" || s.kind === "replace").map((s) => relativeTo(plan.root, dirname5(s.path)));
1786
1805
  if (dep && added.length) {
1787
1806
  manifest.skills[plan.name] = { ...dep, links: [.../* @__PURE__ */ new Set([...dep.links ?? [], ...added])] };
1788
1807
  writeManifest(plan.root, manifest);
@@ -1826,42 +1845,135 @@ function orThrow(r) {
1826
1845
  if (!r.ok) throw new Error(r.message);
1827
1846
  return r.message;
1828
1847
  }
1848
+ function intoLibrary(path, force = false) {
1849
+ const r = importSkill(path, { force });
1850
+ if (r.status === "exists") throw new Error(`${r.name}: your library has a different ${r.name}; update or delete it first`);
1851
+ return r.name;
1852
+ }
1853
+ function track(root, name) {
1854
+ const c = addSkill(root, name);
1855
+ if (c.action === "skipped") throw new Error(`${name}: ${c.reason}`);
1856
+ return c;
1857
+ }
1858
+ var installedIn = (p) => p.projectPath ? ` in ${basename6(p.projectPath)}` : "";
1859
+ var installId = (p) => `${p.projectPath ? `${p.projectPath}:` : ""}${p.id}`;
1860
+ function summarize(where, verb, changes) {
1861
+ const done = changes.filter((c) => c.action !== "skipped");
1862
+ const skipped = changes.filter((c) => c.action === "skipped").map((c) => `${c.name}: ${c.reason}`);
1863
+ if (!done.length) throw new Error(`${where}: nothing ${verb}${skipped.length ? `; ${skipped.join("; ")}` : ""}`);
1864
+ return `${where}: ${verb} ${plural(done.length, "skill")}${skipped.length ? `; skipped ${skipped.join("; ")}` : ""}`;
1865
+ }
1829
1866
  var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
1830
- var agentNames = (ids) => ids.map((id) => HARNESSES.find((h) => h.id === id).name).join(", ");
1867
+ var rows = (root, skills) => skills.map((skill) => ({ root, skill }));
1868
+ var agentNames = (ids) => ids.map((id) => harness(id).name).join(", ");
1831
1869
  function conflictIssue(c) {
1832
1870
  const where = c.root ? basename6(c.root) : "your global folders";
1833
1871
  const winners = c.managed ? c.copies.filter((x) => x.dir === c.managed) : c.copies;
1872
+ const planFor = (dir) => (c.root ? planProjectTidy(c.root, { keep: { [c.name]: dir } }) : planGlobalTidy({ keep: { [c.name]: dir } })).plans.find((p) => p.name === c.name);
1873
+ const keep = (dir, git2) => () => {
1874
+ const plan = planFor(dir);
1875
+ const r = plan ? applyTidy(plan, { git: git2 }) : null;
1876
+ if (!r?.applied.length) throw new Error(`${c.name}: nothing changed${r?.held.length ? " (git would see every change)" : ""}`);
1877
+ return `${c.name}: kept the ${copyLabel(c, dir)} copy${r.held.length ? `; held back ${plural(r.held.length, "change")} git would see` : ""}`;
1878
+ };
1834
1879
  return {
1835
1880
  id: `conflict:${c.root ?? "global"}:${c.name}`,
1836
1881
  severity: "problem",
1837
1882
  title: `${c.name} in ${where}: the copies differ`,
1838
- detail: `Your agents run different versions: ${c.copies.map((x) => `${copyLabel(c, x.dir)}${x.runs.length ? ` (${agentNames(x.runs)})` : ""}`).join(" vs ")}. Keep one; the others become links to it and are restorable from Health.`,
1839
- choices: winners.map((x) => ({
1840
- label: `Keep the ${copyLabel(c, x.dir)} copy`,
1841
- hint: x.dir === c.managed ? "skilllib's version" : "the others become links to it",
1842
- run: () => {
1843
- const keep = { [c.name]: x.dir };
1844
- const plan = (c.root ? planProjectTidy(c.root, { keep }) : planGlobalTidy({ keep })).plans.find((p) => p.name === c.name);
1845
- if (plan) applyTidy(plan, { git: "go" });
1846
- return `${c.name}: kept the ${copyLabel(c, x.dir)} copy`;
1847
- }
1848
- }))
1883
+ detail: `Your agents run different versions: ${c.copies.map((x) => `${copyLabel(c, x.dir)}${x.runs.length ? ` (${agentNames(x.runs)})` : ""}`).join(" vs ")}. Keep one; the others become links to it and are restorable with skilllib restore (Settings \u203A Backups in the app).`,
1884
+ at: rows(c.root, [c.name]),
1885
+ choices: winners.flatMap((x) => {
1886
+ const plan = planFor(x.dir);
1887
+ const visible2 = plan ? gitVisibleSteps(plan).map((s) => describeStep(s, c.root)) : [];
1888
+ const label = `Keep the ${copyLabel(c, x.dir)} copy`;
1889
+ if (!visible2.length) return [{ label, hint: x.dir === c.managed ? "skilllib's version" : "the others become links to it", run: keep(x.dir, "go") }];
1890
+ return [
1891
+ { label: `${label}, but keep git as it is`, hint: `skips what git would see: ${visible2.join(", ")}`, run: keep(x.dir, "keep") },
1892
+ { label: `${label}, and change what git tracks`, hint: `git will see: ${visible2.join(", ")}`, run: keep(x.dir, "go") }
1893
+ ];
1894
+ })
1849
1895
  };
1850
1896
  }
1851
1897
  function applyAll(plans, git2) {
1852
1898
  const results = plans.map((p) => applyTidy(p, { git: git2 }));
1853
1899
  return { skills: results.filter((r) => r.applied.length).length, held: results.reduce((n, r) => n + r.held.length, 0) };
1854
1900
  }
1855
- function findIssues(projects2, machine, libraryNames, statusOf = projectStatus, loadedIn = (root) => skillsLoadedIn(root, machine), nestedOf = nestedSkills) {
1856
- const issues = [];
1901
+ function brokenLinksIn(root) {
1902
+ const where = basename6(root);
1903
+ const tidied = ALL_PROJECT_DIRS.filter((d) => d !== GROK_SKILLS_DIR);
1904
+ const hasCopy = (name) => tidied.some((d) => !isLink(join13(root, d, name)) && existsSync12(join13(root, d, name, "SKILL.md")));
1905
+ let git2;
1906
+ return ALL_PROJECT_DIRS.flatMap((dir) => {
1907
+ const folder = join13(root, dir);
1908
+ if (!existsSync12(folder)) return [];
1909
+ return readdirSync7(folder).flatMap((name) => {
1910
+ const path = join13(folder, name);
1911
+ if (!isLink(path) || existsSync12(path) || dir !== GROK_SKILLS_DIR && hasCopy(name)) return [];
1912
+ const remove2 = {
1913
+ label: "Remove the link",
1914
+ run: () => {
1915
+ unlinkSync4(path);
1916
+ return `Removed the broken link ${dir}/${name} in ${where}`;
1917
+ }
1918
+ };
1919
+ git2 ??= gitInfo(root);
1920
+ const tracked = ["committed", "changed"].includes(git2?.of(relativeTo(root, path)) ?? "");
1921
+ return [
1922
+ {
1923
+ id: `broken:${root}:${dir}/${name}`,
1924
+ severity: "problem",
1925
+ title: `${name}: broken link in ${where}/${dir}`,
1926
+ detail: `It points at a folder that no longer exists, so it loads nothing.${tracked ? " Git tracks the link, so removing it shows in git status." : ""}`,
1927
+ at: rows(root, [name]),
1928
+ ...tracked ? { choices: [remove2] } : { fix: remove2 }
1929
+ }
1930
+ ];
1931
+ });
1932
+ });
1933
+ }
1934
+ function grokDiffers(projects2, machine, statusOf) {
1935
+ if (!enabledHarnesses().includes("grok")) return [];
1936
+ const issue = (root, name, grokCopy, other) => ({
1937
+ id: `grok-differs:${root ?? "global"}:${name}`,
1938
+ severity: "suggestion",
1939
+ title: `${name}${root ? ` in ${basename6(root)}` : ""}: Grok runs a different copy`,
1940
+ detail: `Grok reads ${grokCopy} first; your other agents run ${other}, and the two differ. skilllib doesn't change Grok's folder: update or remove that copy there to make them match.`,
1941
+ at: rows(root, [name])
1942
+ });
1943
+ const grokGlobal = join13(grokDir(), "skills");
1944
+ const global = skillDirsIn(grokGlobal).filter((path) => !isLink(path)).flatMap((path) => {
1945
+ const name = basename6(path);
1946
+ const other = machine.find((m) => m.name === name && m.movable && !m.broken && m.harnesses.some((id) => id !== "grok") && realPath(dirname6(m.path)) !== realPath(grokGlobal));
1947
+ return other && treeHash(other.path) !== treeHash(path) ? [issue(null, name, tildify(path), tildify(other.path))] : [];
1948
+ });
1949
+ const repos = projects2.flatMap(
1950
+ (root) => skillDirsIn(join13(root, GROK_SKILLS_DIR)).filter((path) => !isLink(path)).flatMap((path) => {
1951
+ const name = basename6(path);
1952
+ const other = statusOf(root).find((s) => s.name === name && !s.path.startsWith(join13(root, GROK_SKILLS_DIR)));
1953
+ return other && treeHash(other.path) !== treeHash(path) ? [issue(root, name, `${GROK_SKILLS_DIR}/${name}`, relativeTo(root, other.path))] : [];
1954
+ })
1955
+ );
1956
+ return [...global, ...repos];
1957
+ }
1958
+ function findIssues(roots, machine, libraryNames, statusOf = projectStatus, loadedIn = (root) => skillsLoadedIn(root, machine), nestedOf = nestedSkills) {
1959
+ const { readable: projects2, unreadable } = readableRoots(roots);
1960
+ const issues = unreadable.map(({ root, reason }) => ({
1961
+ id: `manifest:${root}`,
1962
+ severity: "problem",
1963
+ title: `${basename6(root)}: skilllib.json can't be read`,
1964
+ detail: `${reason}. Until it's fixed, skilllib leaves ${basename6(root)} out.`,
1965
+ at: []
1966
+ }));
1857
1967
  const statuses = new Map(projects2.map((root) => [root, statusOf(root)]));
1968
+ issues.push(...grokDiffers(projects2, machine, (root) => statuses.get(root)));
1858
1969
  const nested = new Map(projects2.map((root) => [root, nestedOf(root)]));
1859
1970
  for (const skill of machine.filter((m) => m.broken)) {
1860
1971
  issues.push({
1861
- id: `broken:${skill.name}`,
1972
+ id: `broken:${skill.path}`,
1862
1973
  severity: "problem",
1863
- title: `${skill.name}: broken link in ${tildify(dirname5(skill.path))}`,
1974
+ title: `${skill.name}: broken link in ${tildify(dirname6(skill.path))}`,
1864
1975
  detail: "It points at a folder that no longer exists, so it loads nothing.",
1976
+ at: rows(null, [skill.name]),
1865
1977
  fix: {
1866
1978
  label: "Remove the link",
1867
1979
  run: () => {
@@ -1884,7 +1996,8 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
1884
1996
  id: `dup:${name}`,
1885
1997
  severity: "problem",
1886
1998
  title: `${name}: loaded twice in ${harnesses.join(" and ")}`,
1887
- detail: `${copies.map((c) => `${c.kind} (${c.origin})`).join(" and ")}. Your skill wins: turn the ${vendor.map((c) => c.kind).join(" and ")} copy off at its source${vendor.some((c) => c.kind === "claude.ai") ? " (claude.ai \u2192 Settings \u2192 Skills)" : ""}${vendor.some((c) => c.kind === "system") ? ` (whoever manages ${vendor.find((c) => c.kind === "system").origin})` : ""}.`
1999
+ detail: `${copies.map((c) => `${c.kind} (${c.origin})`).join(" and ")}. Your skill wins: turn the ${vendor.map((c) => c.kind).join(" and ")} copy off at its source${vendor.some((c) => c.kind === "claude.ai") ? " (claude.ai \u2192 Settings \u2192 Skills)" : ""}${vendor.some((c) => c.kind === "system") ? ` (whoever manages ${vendor.find((c) => c.kind === "system").origin})` : ""}.`,
2000
+ at: rows(null, [name])
1888
2001
  });
1889
2002
  }
1890
2003
  const loadedHere = new Map(projects2.map((root) => [root, loadedIn(root)]));
@@ -1929,7 +2042,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
1929
2042
  const pluginName = plugin.id.split("@")[0];
1930
2043
  const remove2 = {
1931
2044
  label: `Remove the plugin ${plugin.id}`,
1932
- hint: plugin.extras.length ? `also removes its ${plugin.extras.join(", ")}` : "restorable from Health",
2045
+ hint: plugin.extras.length ? `also removes its ${plugin.extras.join(", ")}` : "restorable with skilllib restore",
1933
2046
  run: () => orThrow(removePlugin(plugin, "delete"))
1934
2047
  };
1935
2048
  const off = { label: `Turn the plugin ${plugin.id} off`, hint: "stays installed", run: () => orThrow(removePlugin(plugin, "off")) };
@@ -1950,9 +2063,25 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
1950
2063
  plugin.synced ? "It's synced from claude.ai, so it can only be turned off here." : "",
1951
2064
  inRepos.length ? `Or keep it and turn it off only in ${inRepos.map(([root]) => basename6(root)).join(", ")}.` : ""
1952
2065
  ].filter(Boolean).join(" "),
1953
- choices: [...plugin.synced ? [off] : plugin.extras.length ? [off, remove2] : [remove2, off], ...inRepos.flatMap(([root, d]) => repoChoices(plugin, root, d))]
2066
+ choices: [...plugin.synced ? [off] : plugin.extras.length ? [off, remove2] : [remove2, off], ...inRepos.flatMap(([root, d]) => repoChoices(plugin, root, d))],
2067
+ // Global's rows (yours and the plugin's), and the repos where both load.
2068
+ at: [...rows(null, dupes), ...[...inRepos, ...cursorRepos].flatMap(([root, d]) => rows(root, d))],
2069
+ plugin: plugin.id
1954
2070
  });
1955
2071
  }
2072
+ if (enabledHarnesses().includes("claude-code"))
2073
+ for (const p of installedPlugins().filter((x) => x.update && !x.synced && !x.settingsOnly)) {
2074
+ const update2 = { label: p.update === "newer" ? "Update it" : `Update to ${p.update}`, hint: `runs claude plugin update ${p.id}`, run: () => orThrow(updatePlugin(p)) };
2075
+ issues.push({
2076
+ id: `plugin-update:${installId(p)}`,
2077
+ severity: "suggestion",
2078
+ title: `${p.id}${installedIn(p)}: ${p.update === "newer" ? "its marketplace has a newer commit" : `update available (${p.version ?? "?"} \u2192 ${p.update})`}`,
2079
+ detail: "Updates can bring new commands, hooks or MCP servers. If the marketplace asks to run a command, skilllib stops and you update it yourself. Claude Code uses the new version after a restart.",
2080
+ choices: [update2],
2081
+ plugin: p.id,
2082
+ pluginAt: p.projectPath ?? null
2083
+ });
2084
+ }
1956
2085
  const machinePluginSkills = new Set(machinePlugins.flatMap((p) => p.skills));
1957
2086
  for (const root of projects2) {
1958
2087
  const where = basename6(root);
@@ -1967,7 +2096,9 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
1967
2096
  severity: "problem",
1968
2097
  title: `${dupes.join(", ")}: also in the Claude Code plugin ${plugin.id}, on in ${where}`,
1969
2098
  detail: `${where}'s Claude Code settings turn the plugin on, so Claude Code loads both there (the plugin's as /${plugin.id.split("@")[0]}:${dupes[0]}). Your skill wins.`,
1970
- choices: repoChoices(plugin, root, inRepo)
2099
+ choices: repoChoices(plugin, root, inRepo),
2100
+ at: rows(root, dupes),
2101
+ plugin: plugin.id
1971
2102
  });
1972
2103
  }
1973
2104
  }
@@ -1979,7 +2110,9 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
1979
2110
  id: `cursor-plugin:${plugin}`,
1980
2111
  severity: "suggestion",
1981
2112
  title: `${names.join(", ")}: also in the Cursor plugin ${plugin}`,
1982
- detail: `Cursor lists both copies when that plugin is on. Your skill wins: turn the plugin off in Cursor (Settings \u2192 Plugins).`
2113
+ detail: `Cursor lists both copies when that plugin is on. Your skill wins: turn the plugin off in Cursor (Settings \u2192 Plugins).`,
2114
+ at: [...rows(null, names.filter((n) => globalYours.has(n))), ...projects2.flatMap((root) => rows(root, names.filter((n) => statuses.get(root).some((s) => s.name === n))))],
2115
+ plugin
1983
2116
  });
1984
2117
  }
1985
2118
  }
@@ -1989,7 +2122,8 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
1989
2122
  id: "tidy:global",
1990
2123
  severity: "suggestion",
1991
2124
  title: `${plural(global.plans.length, "global skill")} with duplicate copies`,
1992
- 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.`,
2125
+ 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 with skilllib restore.`,
2126
+ at: rows(null, global.plans.map((p) => p.name)),
1993
2127
  fix: {
1994
2128
  label: "Keep one copy of each",
1995
2129
  run: () => {
@@ -2002,13 +2136,14 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
2002
2136
  }
2003
2137
  issues.push(...global.conflicts.map(conflictIssue));
2004
2138
  const kept = keptGlobal();
2005
- const unreviewed = [...new Set(loaded.filter((m) => m.movable && !kept.has(m.name)).map((m) => m.name))];
2139
+ const unreviewed = [...new Set(loaded.filter((m) => m.movable && m.harnesses.length && !kept.has(m.name)).map((m) => m.name))];
2006
2140
  if (unreviewed.length) {
2007
2141
  issues.push({
2008
2142
  id: "review:global",
2009
2143
  severity: "suggestion",
2010
2144
  title: `${plural(unreviewed.length, "global skill")} of yours ${unreviewed.length === 1 ? "loads" : "load"} in every repo`,
2011
- detail: `${unreviewed.join(", ")}. Move each to the repos that need it (skilllib global move <name> --to <repo>...), or mark the ones you want everywhere as global on purpose (skilllib global keep <name>).`
2145
+ detail: `${unreviewed.join(", ")}. Move each to the repos that need it (skilllib global move <name> --to <repo>...), or mark the ones you want everywhere as global on purpose (skilllib global keep <name>).`,
2146
+ at: rows(null, unreviewed)
2012
2147
  });
2013
2148
  }
2014
2149
  const enabled = enabledHarnesses();
@@ -2035,6 +2170,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
2035
2170
  severity: "suggestion",
2036
2171
  title: `${plural(skills.length, "global skill")}${onPurpose ? " you keep global" : ""} not usable by ${agentNames(missing)}`,
2037
2172
  detail: `${skills.map((m) => `${m.name} (only ${agentNames(m.harnesses)})`).join(", ")}. ${onPurpose ? "A link in each agent's global folder makes the one real copy reach every agent you use." : `Link ${skills.length === 1 ? "it" : "them"} for every agent, or move ${skills.length === 1 ? "it" : "them"} to the repos that need ${skills.length === 1 ? "it" : "them"} (skilllib global move), which works for every agent too.`}`,
2173
+ at: rows(null, [...new Set(skills.map((m) => m.name))]),
2038
2174
  ...onPurpose ? { fix: link2 } : { choices: [link2] }
2039
2175
  });
2040
2176
  }
@@ -2044,22 +2180,30 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
2044
2180
  for (const root of projects2) {
2045
2181
  const where = basename6(root);
2046
2182
  const status2 = statuses.get(root);
2047
- const restorable = status2.filter((s) => s.state === "folder missing" && s.latest !== null);
2183
+ issues.push(...brokenLinksIn(root));
2184
+ const pins = readManifest(root).skills;
2185
+ const missing = status2.filter((s) => s.state === "folder missing" && s.latest !== null);
2186
+ const restorable = missing.filter((s) => versionForHash(s.name, pins[s.name]?.hash ?? "") !== null);
2187
+ for (const s of missing.filter((m) => !restorable.includes(m)))
2188
+ issues.push({
2189
+ id: `pin:${root}:${s.name}`,
2190
+ severity: "problem",
2191
+ title: `${s.name}: the version ${where}'s skilllib.json pins isn't in your library`,
2192
+ detail: `The folder is missing, and no version in Your skills has the content it pins (pinned from another library?). Install your newest v${s.latest}; ${where}'s skilllib.json then pins it.`,
2193
+ at: rows(root, [s.name]),
2194
+ // It changes what the shared skilllib.json pins: a choice, never run by `doctor --fix`.
2195
+ choices: [{ label: `Install your newest (v${s.latest})`, hint: `${where}'s skilllib.json pins it`, run: () => `${s.name}: installed v${track(root, s.name).to}` }]
2196
+ });
2048
2197
  if (restorable.length > 0) {
2049
2198
  issues.push({
2050
2199
  id: `sync:${root}`,
2051
2200
  severity: "problem",
2052
2201
  title: `${where}: ${plural(restorable.length, "skill")} in skilllib.json not installed`,
2053
2202
  detail: `${restorable.map((s) => s.name).join(", ")}: the folder is missing`,
2203
+ at: rows(root, restorable.map((s) => s.name)),
2054
2204
  fix: {
2055
2205
  label: "Sync the project",
2056
- run: () => {
2057
- const changes = syncProject(root);
2058
- const done = changes.filter((c) => c.action !== "skipped");
2059
- const skipped = changes.filter((c) => c.action === "skipped").map((c) => `${c.name}: ${c.reason}`);
2060
- if (!done.length) throw new Error(`${where}: nothing restored${skipped.length ? `; ${skipped.join("; ")}` : ""}`);
2061
- return `${where}: restored ${plural(done.length, "skill")}${skipped.length ? `; skipped ${skipped.join("; ")}` : ""}`;
2062
- }
2206
+ run: () => summarize(where, "restored", syncProject(root))
2063
2207
  }
2064
2208
  });
2065
2209
  }
@@ -2069,6 +2213,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
2069
2213
  severity: "problem",
2070
2214
  title: `${s.name}: pinned in ${where}'s skilllib.json, but not in your library`,
2071
2215
  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.`,
2216
+ at: rows(root, [s.name]),
2072
2217
  // Editing skilllib.json is the team's call: a choice, never run by `doctor --fix`.
2073
2218
  choices: [
2074
2219
  {
@@ -2089,15 +2234,10 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
2089
2234
  severity: "suggestion",
2090
2235
  title: `${where}: ${plural(behind.length, "skill")} out of date`,
2091
2236
  detail: behind.map((s) => `${s.name} (v${s.version} \u2192 v${s.latest})`).join(", "),
2237
+ at: rows(root, behind.map((s) => s.name)),
2092
2238
  fix: {
2093
2239
  label: "Update to the newest versions",
2094
- run: () => {
2095
- const changes = updateProject(root, behind.map((s) => s.name));
2096
- const done = changes.filter((c) => c.action !== "skipped");
2097
- const skipped = changes.filter((c) => c.action === "skipped").map((c) => `${c.name}: ${c.reason}`);
2098
- if (!done.length) throw new Error(`${where}: nothing updated${skipped.length ? `; ${skipped.join("; ")}` : ""}`);
2099
- return `${where}: updated ${plural(done.length, "skill")}${skipped.length ? `; skipped ${skipped.join("; ")}` : ""}`;
2100
- }
2240
+ run: () => summarize(where, "updated", updateProject(root, behind.map((s) => s.name)))
2101
2241
  }
2102
2242
  });
2103
2243
  const tidy2 = planProjectTidy(root);
@@ -2113,40 +2253,45 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
2113
2253
  id: `tidy:${root}`,
2114
2254
  severity: "suggestion",
2115
2255
  title: `${where}: ${plural(tidy2.plans.length, "skill")} with extra copies or links`,
2116
- detail: tidy2.plans.map((p) => `${p.name}: ${p.steps.map((s) => describeStep(s, root)).join(", ")}`).join(" \xB7 ") + (visible2.length ? `. Git would see ${plural(visible2.length, "of these change")}: ${visible2.join(", ")}.` : ". Replaced copies are restorable from Health."),
2256
+ detail: tidy2.plans.map((p) => `${p.name}: ${p.steps.map((s) => describeStep(s, root)).join(", ")}`).join(" \xB7 ") + (visible2.length ? `. Git would see ${plural(visible2.length, "of these change")}: ${visible2.join(", ")}.` : ". Replaced copies are restorable with skilllib restore."),
2257
+ at: rows(root, tidy2.plans.map((p) => p.name)),
2117
2258
  ...visible2.length ? {
2118
2259
  choices: [
2119
- { label: "Tidy, but keep git as it is", hint: `skips the ${plural(visible2.length, "change")} git would see`, run: run("keep") },
2260
+ // "Keep git as it is" only when it leaves something to do.
2261
+ ...visible2.length < tidy2.plans.reduce((n, p) => n + p.steps.length, 0) ? [{ label: "Tidy, but keep git as it is", hint: `skips the ${plural(visible2.length, "change")} git would see`, run: run("keep") }] : [],
2120
2262
  { label: "Tidy everything", hint: `${plural(visible2.length, "change")} will show in git status`, run: run("go") }
2121
2263
  ]
2122
2264
  } : { fix: { label: "Tidy", run: run("go") } }
2123
2265
  });
2124
2266
  }
2125
2267
  issues.push(...tidy2.conflicts.map(conflictIssue));
2126
- 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 }) }));
2268
+ const loadsGlobally = (name, id) => globalCopies.some((m) => m.name === name && m.harnesses.includes(id));
2269
+ const blindHere = status2.filter((s) => s.state !== "folder missing" && s.visibility.some((v) => v.paths === 0 && !loadsGlobally(s.name, v.id))).map((s) => ({ s, would: linkEverywhere(root, s.name, s.location, { dryRun: true }) }));
2127
2270
  for (const { s, would } of blindHere.filter(({ would: w }) => !w.created.length && !w.blocked.length && w.uncommitted.length))
2128
2271
  issues.push({
2129
2272
  id: `uncommitted:${root}:${s.name}`,
2130
2273
  severity: "suggestion",
2131
2274
  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`,
2132
- 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).`
2275
+ 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\`).`,
2276
+ at: rows(root, [s.name])
2133
2277
  });
2134
2278
  const partial = blindHere.filter(({ would }) => would.created.length || would.blocked.length).map(({ s }) => s);
2135
2279
  if (partial.length) {
2136
- const missing = [...new Set(partial.flatMap((s) => s.visibility.filter((v) => v.paths === 0).map((v) => v.id)))];
2280
+ const missing2 = [...new Set(partial.flatMap((s) => s.visibility.filter((v) => v.paths === 0).map((v) => v.id)))];
2137
2281
  const tracked = [...new Set(blindHere.flatMap(({ would }) => would.blocked))];
2138
2282
  const run = (allowTracked) => () => {
2139
2283
  if (allowTracked) allowTrackedLinks(root);
2140
2284
  const r = linkAll(root, { allowTracked });
2141
2285
  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)` : ""}`;
2142
2286
  if (!r.linked.length) throw new Error(`${where}: nothing linked${notes}`);
2143
- return `${where}: linked ${plural(r.linked.length, "skill")} for ${agentNames(missing)}${notes}`;
2287
+ return `${where}: linked ${plural(r.linked.length, "skill")} for ${agentNames(missing2)}${notes}`;
2144
2288
  };
2145
2289
  issues.push({
2146
2290
  id: `usable:${root}`,
2147
2291
  severity: "suggestion",
2148
- title: `${where}: ${plural(partial.length, "skill")} not usable by ${agentNames(missing)}`,
2292
+ title: `${where}: ${plural(partial.length, "skill")} not usable by ${agentNames(missing2)}`,
2149
2293
  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.` : ""}`,
2294
+ at: rows(root, partial.map((s) => s.name)),
2150
2295
  ...tracked.length ? {
2151
2296
  choices: [
2152
2297
  // Only when some link goes where git doesn't track: otherwise this one would link nothing.
@@ -2167,12 +2312,13 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
2167
2312
  severity: "suggestion",
2168
2313
  title: `${s.name} in ${where} isn't in your library`,
2169
2314
  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).",
2315
+ at: rows(root, [s.name]),
2170
2316
  choices: [
2171
2317
  {
2172
2318
  label: "Copy into the library",
2173
2319
  hint: `${where}'s copy stays as it is`,
2174
2320
  run: () => {
2175
- importSkill(s.path);
2321
+ intoLibrary(s.path);
2176
2322
  return `${s.name} copied into your library; ${where}'s copy stays as it is`;
2177
2323
  }
2178
2324
  }
@@ -2184,13 +2330,14 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
2184
2330
  severity: "suggestion",
2185
2331
  title: `${s.name} in ${where} isn't in your library`,
2186
2332
  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.",
2333
+ at: rows(root, [s.name]),
2187
2334
  choices: [
2188
2335
  {
2189
2336
  label: "Import into the library",
2190
- hint: `${where} then tracks it`,
2337
+ hint: "copies it into Your skills",
2191
2338
  run: () => {
2192
- importSkill(s.path);
2193
- addSkill(root, s.name);
2339
+ intoLibrary(s.path);
2340
+ track(root, s.name);
2194
2341
  return `${s.name} imported and tracked in ${where}`;
2195
2342
  }
2196
2343
  }
@@ -2202,9 +2349,81 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
2202
2349
  severity: "suggestion",
2203
2350
  title: `${s.name} in ${where} isn't tracked`,
2204
2351
  detail: "It's identical to the library copy; track it (in skilllib.json) so future library changes reach it.",
2205
- choices: [{ label: "Track it", hint: `adds it to ${where}'s skilllib.json`, run: () => `${addSkill(root, s.name).name} tracked in ${where}` }]
2352
+ at: rows(root, [s.name]),
2353
+ choices: [{ label: "Track it", hint: `adds it to ${where}'s skilllib.json`, run: () => `${track(root, s.name).name} tracked in ${where}` }]
2354
+ });
2355
+ } else if (s.state === "untracked, differs from library" || s.state === "repo skill, differs from library") {
2356
+ const teams = s.state.startsWith("repo skill") || committed(s);
2357
+ issues.push({
2358
+ id: `differs:${root}:${s.name}`,
2359
+ severity: "suggestion",
2360
+ title: `${s.name} in ${where} differs from your library`,
2361
+ detail: teams ? `It's the team's copy (committed, or in the repo's own .agents/skills), and no version in Your skills matches it. Update your library from it if it's the one to keep; ${where}'s copy stays as it is.` : `No version in Your skills matches it. Update your library from it (${where} then tracks it), or replace it with your library's.`,
2362
+ at: rows(root, [s.name]),
2363
+ choices: [
2364
+ {
2365
+ label: "Update your library from it",
2366
+ hint: teams ? `a new version in Your skills; ${where}'s copy stays as it is` : `a new version in Your skills; ${where} then tracks it`,
2367
+ run: () => {
2368
+ intoLibrary(s.path, true);
2369
+ if (teams) return `${s.name}: your library has ${where}'s copy; ${where}'s copy stays as it is`;
2370
+ const c = track(root, s.name);
2371
+ return `${s.name}: your library has ${where}'s copy as v${c.to}; ${where} tracks it`;
2372
+ }
2373
+ },
2374
+ ...teams ? [] : [
2375
+ {
2376
+ label: "Replace it with your library's",
2377
+ hint: "this copy is restorable with skilllib restore",
2378
+ run: () => {
2379
+ const c = addSkill(root, s.name, { force: true });
2380
+ if (c.action === "skipped") throw new Error(`${s.name}: ${c.reason}`);
2381
+ return `${s.name} in ${where} is your library's v${c.to} now; the old copy is restorable with skilllib restore`;
2382
+ }
2383
+ }
2384
+ ]
2385
+ ]
2386
+ });
2387
+ } else if (s.state === "from npx skills") {
2388
+ const teams = committed(s);
2389
+ issues.push({
2390
+ id: `npx:${root}:${s.name}`,
2391
+ severity: "suggestion",
2392
+ title: `${s.name} in ${where} came from npx skills, not your library`,
2393
+ detail: `Installed with \`npx skills add\`${s.source ? ` (${s.source})` : ""}. Import it into your library to reuse it in other projects and keep it backed up${teams ? `; ${where}'s copy stays as it is (committed)` : `; ${where} then tracks it in skilllib.json`}.`,
2394
+ at: rows(root, [s.name]),
2395
+ choices: [
2396
+ {
2397
+ label: "Import it into your library",
2398
+ hint: teams ? `${where}'s copy stays as it is` : `${where} then tracks it`,
2399
+ run: () => {
2400
+ intoLibrary(s.path);
2401
+ if (teams) return `${s.name} copied into your library; ${where}'s copy stays as it is`;
2402
+ track(root, s.name);
2403
+ return `${s.name} imported and tracked in ${where}`;
2404
+ }
2405
+ }
2406
+ ]
2206
2407
  });
2207
2408
  }
2409
+ if (s.state === "not in library")
2410
+ issues.push({
2411
+ id: `not-in-lib:${root}:${s.name}`,
2412
+ severity: "suggestion",
2413
+ title: `${s.name} in ${where} is tracked, but no longer in your library`,
2414
+ detail: `${where}'s skilllib.json pins it, but your library has no copy: if the folder goes, sync can't restore it, and your other repos can't add it. Copy ${where}'s copy back into the library.`,
2415
+ at: rows(root, [s.name]),
2416
+ choices: [
2417
+ {
2418
+ label: "Copy it back into your library",
2419
+ hint: `${where}'s copy stays as it is`,
2420
+ run: () => {
2421
+ intoLibrary(s.path);
2422
+ return `${s.name} copied back into your library`;
2423
+ }
2424
+ }
2425
+ ]
2426
+ });
2208
2427
  if (s.state === "edited locally" || s.state === "edited locally, update available") {
2209
2428
  const newer2 = s.state !== "edited locally";
2210
2429
  const pinned = s.version ? `v${s.version}` : "the library version";
@@ -2213,6 +2432,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
2213
2432
  severity: "suggestion",
2214
2433
  title: `${s.name} in ${where} has local edits`,
2215
2434
  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.`,
2435
+ at: rows(root, [s.name]),
2216
2436
  choices: [
2217
2437
  {
2218
2438
  label: "Save as a new version in Your skills",
@@ -2226,7 +2446,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
2226
2446
  },
2227
2447
  {
2228
2448
  label: "Discard the edits",
2229
- hint: `back to ${pinned}; the edited copy is restorable from Health`,
2449
+ hint: `back to ${pinned}; the edited copy is restorable with skilllib restore`,
2230
2450
  run: () => {
2231
2451
  const c = discardEdits(root, s.name);
2232
2452
  if (c.action === "skipped") throw new Error(`${s.name}: ${c.reason}`);
@@ -2242,13 +2462,14 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
2242
2462
  for (const root of projects2) {
2243
2463
  for (const n of nested.get(root)) {
2244
2464
  const copies = alsoGlobal.get(n.name) ?? [];
2245
- if (alsoLoadedGlobally(n) && !copies.some((c) => c.root === root)) alsoGlobal.set(n.name, [...copies, { root, managed: false }]);
2465
+ if (alsoLoadedGlobally(n) && !copies.some((c) => c.root === root)) alsoGlobal.set(n.name, [...copies, { root, managed: false, nested: true }]);
2246
2466
  }
2247
2467
  }
2248
2468
  for (const [name, copies] of alsoGlobal) {
2249
2469
  const wheres = copies.map((c) => basename6(c.root)).join(", ");
2250
2470
  const removable = copies.filter((c) => c.managed).map((c) => c.root);
2251
2471
  const repoOwn = copies.filter((c) => !c.managed).map((c) => basename6(c.root));
2472
+ const at = [...copies.flatMap((c) => rows(c.root, [name])), ...rows(null, [name])];
2252
2473
  if (kept.has(name)) {
2253
2474
  const removeWheres = removable.map((r) => basename6(r)).join(", ");
2254
2475
  issues.push({
@@ -2256,13 +2477,15 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
2256
2477
  severity: "suggestion",
2257
2478
  title: `${name}: in ${wheres}, and you keep it global`,
2258
2479
  detail: `The global copy already loads there, so the project copies are extra. Claude Code runs the global one anyway.${repoOwn.length ? ` ${repoOwn.join(", ")} ${repoOwn.length === 1 ? "has its own copy" : "have their own copies"}: remove ${repoOwn.length === 1 ? "it" : "them"} in the repo, or stop keeping ${name} global.` : ""}`,
2480
+ at,
2259
2481
  choices: removable.length ? [
2260
2482
  {
2261
2483
  label: `Remove the copies in ${removeWheres}`,
2262
2484
  hint: "the global copy stays",
2263
2485
  run: () => {
2264
2486
  const removed = removable.filter((root) => removeSkill(root, name).action === "removed").map((r) => basename6(r));
2265
- return `${name}: removed from ${removed.join(", ") || "no project"}; the global copy stays`;
2487
+ if (!removed.length) throw new Error(`${name}: removed from no project (local edits?)`);
2488
+ return `${name}: removed from ${removed.join(", ")}; the global copy stays`;
2266
2489
  }
2267
2490
  }
2268
2491
  ] : void 0
@@ -2271,18 +2494,23 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
2271
2494
  }
2272
2495
  const movable = machine.find((m) => m.name === name && m.movable);
2273
2496
  issues.push({
2497
+ at,
2274
2498
  id: `twice:${name}`,
2275
2499
  severity: "suggestion",
2276
2500
  title: `${name}: in ${wheres} and also loaded globally`,
2277
- detail: `${repoOwn.length === copies.length ? `Agents there load both the repo's copy and the global one.` : "The project copies are enough; the global one loads it in every other project too."} ${movable ? "Stop loading it globally, or keep it global on purpose (Global \u2192 Enter)." : `The global copy comes from ${machine.find((m) => m.name === name && !m.movable)?.origin ?? "a vendor"}: turn it off there.`}`,
2501
+ detail: `${repoOwn.length === copies.length ? `Agents there load both the repo's copy and the global one.` : "The project copies are enough; the global one loads it in every other project too."} ${movable ? "Stop loading it globally, or keep it global on purpose (skilllib global keep)." : `The global copy comes from ${machine.find((m) => m.name === name && !m.movable)?.origin ?? "a vendor"}: turn it off there.`}`,
2278
2502
  choices: movable ? [
2279
2503
  {
2280
2504
  label: "Stop loading it globally",
2281
- hint: "restorable from Health",
2505
+ hint: "restorable with skilllib restore",
2282
2506
  run: () => {
2507
+ const blind = copies.filter((c) => !c.nested).flatMap((c) => visibilityOf(c.root, name).flatMap((v) => v.paths === 0 ? [`${harness(v.id).name} in ${basename6(c.root)}`] : []));
2508
+ if (blind.length) throw new Error(`${name}: ${blind.join(", ")} only see the global copy; link the repo copies first (skilllib link)`);
2283
2509
  if (!libraryNames.has(name)) importSkill(movable.path);
2284
- const r = unloadGlobal(movable.path, movable.links);
2285
- if (!r.ok) throw new Error(`${name}: ${r.reason}`);
2510
+ for (const m of machine.filter((x) => x.name === name && x.movable)) {
2511
+ const r = unloadGlobal(m.path, m.links);
2512
+ if (!r.ok) throw new Error(`${name}: ${r.reason}`);
2513
+ }
2286
2514
  return `${name} no longer loads globally`;
2287
2515
  }
2288
2516
  }
@@ -2305,6 +2533,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
2305
2533
  severity: "suggestion",
2306
2534
  title: agentSkill2 === "missing" ? "Your agents don't know about skilllib" : "The skilllib skill for your agents is out of date",
2307
2535
  detail: "The skilllib skill lets you ask your agents which skills they can use here, where each comes from, and which of your skills a repo should add.",
2536
+ at: rows(null, ["skilllib"]),
2308
2537
  ...agentSkill2 === "missing" ? { choices: [install] } : { fix: install }
2309
2538
  });
2310
2539
  }
@@ -2332,6 +2561,7 @@ function usageIssues(projects2, machine, libraryNames, uses, statusOf = projectS
2332
2561
  severity: "suggestion",
2333
2562
  title: `${where}: ${plural(unused.length, "skill")} unused in ${uses.days} days`,
2334
2563
  detail: `${unused.map((s) => s.name).join(", ")}: no Claude Code session in ${where} used ${unused.length === 1 ? "it" : "them"}. Each one's name and description still takes context in every session there.${unused.length > removable.length ? " The repo's own skills are the team's: remove those in the repo if nobody needs them." : ""}`,
2564
+ at: rows(root, unused.map((s) => s.name)),
2335
2565
  choices: removable.length ? [
2336
2566
  {
2337
2567
  label: `Remove ${removable.length === unused.length ? "them" : `the ${plural(removable.length, "skill")} skilllib installed`} from ${where}`,
@@ -2340,12 +2570,30 @@ function usageIssues(projects2, machine, libraryNames, uses, statusOf = projectS
2340
2570
  const changes = removable.map((s) => removeSkill(root, s.name));
2341
2571
  const removed = changes.filter((c) => c.action === "removed").map((c) => c.name);
2342
2572
  const kept = changes.filter((c) => c.action !== "removed").map((c) => c.name);
2343
- return `${where}: removed ${removed.join(", ") || "nothing"}${kept.length ? `; kept ${kept.join(", ")} (local edits)` : ""}`;
2573
+ if (!removed.length) throw new Error(`${where}: removed nothing; kept ${kept.join(", ")} (local edits)`);
2574
+ return `${where}: removed ${removed.join(", ")}${kept.length ? `; kept ${kept.join(", ")} (local edits)` : ""}`;
2344
2575
  }
2345
2576
  }
2346
2577
  ] : void 0
2347
2578
  });
2348
2579
  }
2580
+ if (uses.asUsed)
2581
+ for (const p of installedPlugins().filter((x) => x.agent === "claude-code" && x.on && x.skills.length)) {
2582
+ const name = p.id.split("@")[0];
2583
+ if (p.skills.some((path) => uses.asUsed(`${name}:${basename6(path)}`) > 0)) continue;
2584
+ issues.push({
2585
+ id: `plugin-unused:${installId(p)}`,
2586
+ severity: "suggestion",
2587
+ title: `${p.id}${installedIn(p)}: its skills weren't used in ${uses.days} days`,
2588
+ detail: `No Claude Code session used ${p.skills.map((path) => `/${name}:${basename6(path)}`).join(", ")}. Each one's name and description still takes context in every session.${p.extras.length ? ` It also brings ${p.extras.join(", ")}: turning it off pauses those too.` : ""}`,
2589
+ choices: [
2590
+ { label: `Turn the plugin ${p.id} off`, hint: "stays installed", run: () => orThrow(setPluginOn(p, false)) },
2591
+ ...p.synced || p.settingsOnly ? [] : [{ label: `Uninstall the plugin ${p.id}`, hint: "its data stays; restorable with skilllib restore", run: () => orThrow(uninstallPlugin(p)) }]
2592
+ ],
2593
+ plugin: p.id,
2594
+ pluginAt: p.projectPath ?? null
2595
+ });
2596
+ }
2349
2597
  const installed = /* @__PURE__ */ new Set([...projects2.flatMap((root) => statusOf(root).map((s) => s.name)), ...machine.filter((m) => !m.broken).map((m) => m.name)]);
2350
2598
  const idle = [...libraryNames].filter((name) => {
2351
2599
  const first = versionHistory(name)[0];
@@ -2357,13 +2605,16 @@ function usageIssues(projects2, machine, libraryNames, uses, statusOf = projectS
2357
2605
  severity: "suggestion",
2358
2606
  title: `${plural(idle.length, "skill")} in Your skills: in no repo, unused in ${uses.days} days`,
2359
2607
  detail: `${idle.sort().join(", ")}. They load nowhere, so they take no context: add the ones you want to a repo, or delete the ones you've outgrown.`,
2608
+ // Your skills' rows (none loads globally, so none is in Global).
2609
+ at: rows(null, idle),
2360
2610
  choices: [
2361
2611
  {
2362
2612
  label: `Delete ${idle.length === 1 ? "it" : `all ${idle.length}`} from Your skills`,
2363
- hint: "restorable from Health",
2613
+ hint: "restorable with skilllib restore",
2364
2614
  run: () => {
2365
2615
  const done = idle.map((name) => [name, deleteLibrarySkill(name)]);
2366
2616
  const failed = done.filter(([, r]) => !r.ok).map(([name]) => name);
2617
+ if (failed.length === done.length) throw new Error(`Deleted nothing; kept ${failed.join(", ")}`);
2367
2618
  return `Deleted ${plural(done.length - failed.length, "skill")} from Your skills${failed.length ? `; kept ${failed.join(", ")}` : ""}`;
2368
2619
  }
2369
2620
  }
@@ -2391,7 +2642,7 @@ function globalFrom(kind, origin, path) {
2391
2642
  var SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "target", "vendor", "coverage"]);
2392
2643
  function topLevel(root) {
2393
2644
  try {
2394
- return readdirSync7(root, { withFileTypes: true }).filter((d) => !d.name.startsWith(".") && !SKIP.has(d.name)).map((d) => d.isDirectory() ? `${d.name}/` : d.name).sort().slice(0, 60);
2645
+ return readdirSync8(root, { withFileTypes: true }).filter((d) => !d.name.startsWith(".") && !SKIP.has(d.name)).map((d) => d.isDirectory() ? `${d.name}/` : d.name).sort().slice(0, 60);
2395
2646
  } catch {
2396
2647
  return [];
2397
2648
  }
@@ -2407,7 +2658,7 @@ function usableHere(root, uses = /* @__PURE__ */ new Map()) {
2407
2658
  const group = (list2) => groupBy(
2408
2659
  list2.map((s) => {
2409
2660
  const loadedBy = s.visibility.filter((v) => v.paths > 0).map((v) => v.id);
2410
- const behind = s.managed && s.latest !== null && s.version !== null && s.latest > s.version;
2661
+ const behind = s.state.endsWith("update available");
2411
2662
  const gitState = git2?.of(relativeTo(root, s.path));
2412
2663
  const repo = !s.managed && (s.state.startsWith("repo skill") || gitState === "committed" || gitState === "changed");
2413
2664
  const usual = s.state === "ok" || s.state === "local only" || repo && s.state !== "from npx skills";
@@ -2445,7 +2696,8 @@ function usableHere(root, uses = /* @__PURE__ */ new Map()) {
2445
2696
  const usesHere = Object.fromEntries([...uses].filter(([, n]) => n > 0));
2446
2697
  return {
2447
2698
  project: tildify(root),
2448
- files: topLevel(root),
2699
+ // Outside a repo there are no project files to read (and the folder may be your home).
2700
+ files: inRepo ? topLevel(root) : [],
2449
2701
  agents,
2450
2702
  skills,
2451
2703
  nested: nested.length > 0 ? nested : void 0,
@@ -2460,7 +2712,7 @@ function firstSentence(description) {
2460
2712
  return sentence.length > 200 ? sentence.slice(0, 199).trimEnd() + "\u2026" : sentence;
2461
2713
  }
2462
2714
  function libraryFor(currentProject) {
2463
- const manifests = visibleProjects().map((p) => ({ name: basename7(p), skills: readManifest(p).skills }));
2715
+ const manifests = visibleProjects().map((p) => ({ name: basename7(p), skills: pinsIn(p) }));
2464
2716
  const here = currentProject ? new Set(projectStatus(currentProject).map((s) => s.name)) : null;
2465
2717
  const all = librarySkills();
2466
2718
  const describe2 = (s) => {
@@ -2472,8 +2724,8 @@ function libraryFor(currentProject) {
2472
2724
  }
2473
2725
 
2474
2726
  // src/usage.ts
2475
- import { createReadStream, existsSync as existsSync12, readdirSync as readdirSync8, statSync as statSync4 } from "fs";
2476
- import { join as join13 } from "path";
2727
+ import { createReadStream, existsSync as existsSync13, readdirSync as readdirSync9, statSync as statSync5 } from "fs";
2728
+ import { join as join14 } from "path";
2477
2729
  import { createInterface } from "readline";
2478
2730
  var BUILTIN_COMMANDS = /* @__PURE__ */ new Set([
2479
2731
  "add-dir",
@@ -2553,12 +2805,12 @@ function usesFromTranscriptLine(line) {
2553
2805
  return [];
2554
2806
  }
2555
2807
  function transcriptFiles(sinceMs) {
2556
- const base = join13(claudeDir(), "projects");
2557
- if (!existsSync12(base)) return [];
2558
- return readdirSync8(base).flatMap((project) => {
2559
- const dir = join13(base, project);
2560
- if (!statSync4(dir).isDirectory()) return [];
2561
- return readdirSync8(dir).filter((f) => f.endsWith(".jsonl")).map((f) => join13(dir, f)).filter((f) => statSync4(f).mtimeMs >= sinceMs);
2808
+ const base = join14(claudeDir(), "projects");
2809
+ if (!existsSync13(base)) return [];
2810
+ return readdirSync9(base).flatMap((project) => {
2811
+ const dir = join14(base, project);
2812
+ if (!statSync5(dir).isDirectory()) return [];
2813
+ return readdirSync9(dir).filter((f) => f.endsWith(".jsonl")).map((f) => join14(dir, f)).filter((f) => statSync5(f).mtimeMs >= sinceMs);
2562
2814
  });
2563
2815
  }
2564
2816
  async function scanUsage(days) {
@@ -2574,7 +2826,7 @@ async function scanUsage(days) {
2574
2826
  }
2575
2827
  return [...seen.values()];
2576
2828
  }
2577
- function summarize(uses, projectOf = () => null) {
2829
+ function summarize2(uses, projectOf = () => null) {
2578
2830
  const bySkill = /* @__PURE__ */ new Map();
2579
2831
  for (const use of uses) {
2580
2832
  const skill = use.skill.split(":").pop();
@@ -2601,7 +2853,12 @@ function usesByProject(uses, projectOf) {
2601
2853
 
2602
2854
  // src/move.ts
2603
2855
  import { basename as basename8 } from "path";
2604
- function moveGlobalSkills(skills, roots, { group, allowTracked = false, label = basename8 } = {}) {
2856
+ function moveGlobalSkills(skills, roots, {
2857
+ group,
2858
+ allowTracked = false,
2859
+ label = basename8,
2860
+ allowBlind = false
2861
+ } = {}) {
2605
2862
  const byName = /* @__PURE__ */ new Map();
2606
2863
  for (const m of skills) byName.set(m.name, [...byName.get(m.name) ?? [], m]);
2607
2864
  return [...byName].map(([name, copies]) => {
@@ -2612,7 +2869,14 @@ function moveGlobalSkills(skills, roots, { group, allowTracked = false, label =
2612
2869
  if (group) recordOrigin(name, `group: ${group}`);
2613
2870
  const changes = roots.map((root) => [root, addSkill(root, name, { allowTracked })]);
2614
2871
  const skipped = changes.filter(([, c]) => c.action === "skipped");
2615
- if (skipped.length) return { name, moved: false, reason: `not added to ${skipped.map(([r, c]) => `${label(r)} (${c.reason})`).join(", ")}, so it still loads globally`, changes };
2872
+ const why = (reason = "") => reason.replace(/ \(use --force to replace it\)$/, ": move or remove it there first");
2873
+ if (skipped.length) return { name, moved: false, reason: `not added to ${skipped.map(([r, c]) => `${label(r)} (${why(c.reason)})`).join(", ")}, so it still loads globally`, changes };
2874
+ const held = changes.filter(([, c]) => c.blocked?.length || c.uncommitted?.length);
2875
+ const blind = held.flatMap(([root]) => visibilityOf(root, name).flatMap((v) => v.paths === 0 ? [`${harness(v.id).name} in ${label(root)}`] : []));
2876
+ if (blind.length && !allowBlind) {
2877
+ const next = held.some(([, c]) => c.uncommitted?.length) ? "commit the skill's copy in the repo, then run skilllib link --allow-tracked there" : "re-run with --allow-tracked";
2878
+ return { name, moved: false, reason: `added, but ${blind.join(", ")} couldn't see it (git tracks the folder its link goes in), so it still loads globally; ${next}`, changes };
2879
+ }
2616
2880
  const failed = copies.map((m) => unloadGlobal(m.path, m.links)).find((r) => !r.ok);
2617
2881
  return failed && !failed.ok ? { name, moved: false, reason: failed.reason, changes } : { name, moved: true, changes };
2618
2882
  });
@@ -2636,6 +2900,7 @@ var FLAGS = {
2636
2900
  function parseArgs(argv) {
2637
2901
  const { values: v, positionals } = parseFlags({ args: argv, options: FLAGS, allowPositionals: true, strict: true });
2638
2902
  const days = Number(v.days ?? USAGE_DAYS);
2903
+ if (!Number.isInteger(days) || days <= 0) throw new Error(`--days takes a whole number of days, like --days 30 (got ${v.days})`);
2639
2904
  return {
2640
2905
  positional: positionals,
2641
2906
  force: !!v.force,
@@ -2644,7 +2909,7 @@ function parseArgs(argv) {
2644
2909
  fix: !!v.fix,
2645
2910
  allowTracked: !!v["allow-tracked"],
2646
2911
  json: !!v.json,
2647
- days: Number.isFinite(days) && days > 0 ? days : USAGE_DAYS,
2912
+ days,
2648
2913
  dryRun: !!v["dry-run"],
2649
2914
  allowGit: !!v["allow-git"],
2650
2915
  ...v.keep ? { keep: v.keep } : {},
@@ -2660,25 +2925,24 @@ function colorState(state) {
2660
2925
  function withDirs(root, changes) {
2661
2926
  const { skills } = readManifest(root);
2662
2927
  return changes.map((c) => {
2663
- const dep = c.action === "installed" || c.action === "updated" || c.action === "reset" ? skills[c.name] : void 0;
2928
+ const dep = c.action === "installed" || c.action === "updated" || c.action === "reset" || c.action === "relinked" ? skills[c.name] : void 0;
2664
2929
  return dep ? { ...c, dirs: [dep.dir ?? ".claude/skills", ...dep.links ?? []] } : c;
2665
2930
  });
2666
2931
  }
2932
+ var incomplete = (c) => c.action === "skipped" || !!c.blocked?.length || !!c.uncommitted?.length;
2933
+ function warnHeldLinks(c, where = "here") {
2934
+ if (c.blocked?.length) warn(`${c.name}: git tracks ${c.blocked.join(", ")} ${where}, so no link was added there. Re-run with --allow-tracked to add it.`);
2935
+ if (c.uncommitted?.length)
2936
+ warn(`${c.name}: git tracks ${c.uncommitted.join(", ")} ${where} 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.`);
2937
+ }
2667
2938
  function printChanges(root, changes, args) {
2668
- if (changes.some((c) => c.action === "skipped")) process.exitCode = 1;
2939
+ if (changes.some(incomplete)) process.exitCode = 1;
2669
2940
  if (args.json) return json("changes", withDirs(root, changes));
2670
2941
  for (const c of changes) {
2671
- if (c.blocked?.length) {
2672
- warn(`${c.name}: git tracks ${c.blocked.join(", ")} here, so no link was added there. Re-run with --allow-tracked to add it.`);
2673
- }
2674
- if (c.uncommitted?.length) {
2675
- warn(
2676
- `${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.`
2677
- );
2678
- }
2942
+ warnHeldLinks(c);
2679
2943
  if (c.action === "skipped") error(`${c.name}: ${c.reason}`);
2680
2944
  else {
2681
- const icon = c.action === "removed" ? red("-") : c.action === "reset" ? yellow("\u21BA") : green(c.action === "installed" ? "+" : "\u2191");
2945
+ const icon = c.action === "removed" ? red("-") : c.action === "reset" ? yellow("\u21BA") : green(c.action === "installed" ? "+" : c.action === "relinked" ? "\u21E2" : "\u2191");
2682
2946
  const versions = c.action === "removed" ? "" : c.from && c.from !== c.to ? ` v${c.from} \u2192 v${c.to}` : c.to ? ` v${c.to}` : "";
2683
2947
  info(` ${icon} ${c.name}${versions} ${dim(c.action)}`);
2684
2948
  }
@@ -2693,9 +2957,9 @@ function projectOfFactory(projects2) {
2693
2957
  const sorted = [...projects2].sort((a, b) => b.length - a.length);
2694
2958
  return (cwd) => {
2695
2959
  if (!cwd) return null;
2696
- const known = sorted.find((p) => cwd === p || cwd.startsWith(p + sep5));
2960
+ const known = sorted.find((p) => cwd === p || cwd.startsWith(p + sep6));
2697
2961
  if (known) return known;
2698
- const repo = cwd.split(`${sep5}.claude${sep5}worktrees${sep5}`)[0];
2962
+ const repo = cwd.split(`${sep6}.claude${sep6}worktrees${sep6}`)[0];
2699
2963
  return isProjectCandidate(repo) ? repo : null;
2700
2964
  };
2701
2965
  }
@@ -2703,13 +2967,23 @@ async function usageBySkill(days, onlyProject) {
2703
2967
  const projectOf = projectOfFactory(onlyProject ? [onlyProject, ...knownProjects()] : knownProjects());
2704
2968
  const uses = await scanUsage(days);
2705
2969
  const scoped = onlyProject ? uses.filter((u) => projectOf(u.cwd) === onlyProject) : uses;
2706
- return new Map(summarize(scoped, projectOf).map((s) => [s.skill, s]));
2970
+ return new Map(summarize2(scoped, projectOf).map((s) => [s.skill, s]));
2707
2971
  }
2972
+ var projectsFor = (args) => {
2973
+ if (!args.all) return [requireProject()];
2974
+ const { readable, unreadable } = readableRoots(visibleProjects());
2975
+ for (const u of unreadable) {
2976
+ error(`${basename9(u.root)}: ${u.reason}`);
2977
+ process.exitCode = 1;
2978
+ }
2979
+ return readable;
2980
+ };
2981
+ var yourGlobals = () => machineSkills().filter((m) => m.movable && !m.broken);
2708
2982
  function requireProject() {
2709
2983
  const root = projectHere();
2710
2984
  if (root) return root;
2711
2985
  if (!isProjectCandidate(findProjectRoot())) {
2712
- const stray = existsSync13(join14(userHome(), MANIFEST_FILE)) ? `, though ${tildify(join14(userHome(), MANIFEST_FILE))} makes it look like one` : "";
2986
+ const stray = existsSync14(join15(userHome(), MANIFEST_FILE)) ? `, though ${tildify(join15(userHome(), MANIFEST_FILE))} makes it look like one` : "";
2713
2987
  throw new Error(`Your home folder isn't a project${stray}: skills added here would load in every repo. Run this inside a project.`);
2714
2988
  }
2715
2989
  throw new Error("Not in a git repo. Run this inside a project (or `git init` first).");
@@ -2732,14 +3006,8 @@ async function status(args) {
2732
3006
  `);
2733
3007
  if (skills.length === 0 && nested.length === 0) info("No skills in this project yet. Add one from your library with: skilllib add <name>");
2734
3008
  const usage2 = await usageBySkill(args.days, root);
2735
- const git2 = gitInfo(root, [...new Set(nested.map((s) => s.location))]);
2736
- const committed = (path) => ["committed", "changed"].includes(git2?.of(relativeTo(root, path)) ?? "");
2737
- const asRepo = {
2738
- "local only": "repo skill",
2739
- "untracked copy of library skill": "repo skill, in library",
2740
- "untracked, differs from library": "repo skill, differs from library"
2741
- };
2742
- const stateOf = (s) => colorState(committed(s.path) && asRepo[s.state] || s.state);
3009
+ const teamState = teamStateIn(root, nested.map((s) => s.location));
3010
+ const stateOf = (s) => colorState(teamState(s));
2743
3011
  const agentsOf = (s) => {
2744
3012
  const ids = s.visibility.filter((v) => v.paths > 0).map((v) => v.id);
2745
3013
  return ids.length === s.visibility.length ? dim("all") : ids.length ? ids.map((id) => harness(id).name).join(", ") : red("none");
@@ -2804,7 +3072,7 @@ async function list(args) {
2804
3072
  } else {
2805
3073
  const installs = /* @__PURE__ */ new Map();
2806
3074
  for (const project of visibleProjects()) {
2807
- for (const name of Object.keys(readManifest(project).skills)) {
3075
+ for (const name of Object.keys(pinsIn(project))) {
2808
3076
  installs.set(name, [...installs.get(name) ?? [], basename9(project)]);
2809
3077
  }
2810
3078
  }
@@ -2823,7 +3091,7 @@ async function list(args) {
2823
3091
  );
2824
3092
  }
2825
3093
  const libraryNames = new Set(skills.map((s) => s.name));
2826
- const globals = [...new Set(machineSkills().filter((m) => m.movable && !m.broken).map((m) => m.name))];
3094
+ const globals = [...new Set(yourGlobals().map((m) => m.name))];
2827
3095
  if (globals.length > 0) {
2828
3096
  info("");
2829
3097
  warn(`${globals.length} of your global ${globals.length === 1 ? "skill still loads" : "skills still load"} in every project`);
@@ -2835,20 +3103,33 @@ async function list(args) {
2835
3103
  );
2836
3104
  }
2837
3105
  }
3106
+ function teamStateIn(root, extraDirs = []) {
3107
+ const git2 = gitInfo(root, [...new Set(extraDirs)]);
3108
+ const committed = (path) => ["committed", "changed"].includes(git2?.of(relativeTo(root, path)) ?? "");
3109
+ const asRepo = {
3110
+ "local only": "repo skill",
3111
+ "untracked copy of library skill": "repo skill, in library",
3112
+ "untracked, differs from library": "repo skill, differs from library"
3113
+ };
3114
+ return (s) => committed(s.path) && asRepo[s.state] || s.state;
3115
+ }
2838
3116
  function projects(args) {
2839
- const roots = visibleProjects();
3117
+ const { readable: roots, unreadable } = readableRoots(visibleProjects());
3118
+ for (const u of unreadable) {
3119
+ error(`${basename9(u.root)}: ${u.reason}`);
3120
+ process.exitCode = 1;
3121
+ }
2840
3122
  if (args.json) {
2841
3123
  return json(
2842
3124
  "projects",
2843
- roots.map(
2844
- (root) => ({
3125
+ roots.map((root) => {
3126
+ const teamState = teamStateIn(root);
3127
+ return {
2845
3128
  name: basename9(root),
2846
3129
  path: root,
2847
- skills: projectStatus(root).map(
2848
- (s) => ({ name: s.name, managed: s.managed, state: s.state, version: s.version, latest: s.latest })
2849
- )
2850
- })
2851
- )
3130
+ skills: projectStatus(root).map((s) => ({ name: s.name, managed: s.managed, state: teamState(s), version: s.version, latest: s.latest }))
3131
+ };
3132
+ })
2852
3133
  );
2853
3134
  }
2854
3135
  if (roots.length === 0) {
@@ -2886,59 +3167,60 @@ function remove(args) {
2886
3167
  const root = requireProject();
2887
3168
  printChanges(root, args.positional.map((name) => removeSkill(root, name, { force: args.force })), args);
2888
3169
  }
2889
- function sync(args) {
2890
- if (args.positional.length) throw new Error(`sync takes no skill names: it installs exactly what skilllib.json pins. To add a skill: skilllib add ${args.positional.join(" ")}`);
2891
- const roots = args.all ? visibleProjects() : [requireProject()];
3170
+ function changeEach(args, change, nothing) {
3171
+ const roots = projectsFor(args);
2892
3172
  const results = roots.map((root) => {
2893
3173
  if (args.allowTracked) allowTrackedLinks(root);
2894
- const changes = syncProject(root, { force: args.force });
2895
- if (changes.some((c) => c.action === "skipped")) process.exitCode = 1;
3174
+ const changes = change(root);
3175
+ if (changes.some(incomplete)) process.exitCode = 1;
2896
3176
  if (!args.json) {
2897
3177
  if (args.all) info(`${basename9(root)} ${dim(tildify(root))}`);
2898
- if (changes.length === 0) {
2899
- if (existsSync13(join14(root, MANIFEST_FILE))) success("Installed versions match skilllib.json");
2900
- else info(dim("No skilllib.json: nothing to sync"));
2901
- } else printChanges(root, changes, args);
3178
+ if (changes.length === 0) nothing(root);
3179
+ else printChanges(root, changes, args);
2902
3180
  }
2903
3181
  return { project: basename9(root), path: root, changes: withDirs(root, changes) };
2904
3182
  });
2905
3183
  if (args.json) args.all ? json("changes --all", results) : json("changes", results[0].changes);
3184
+ return roots;
3185
+ }
3186
+ function sync(args) {
3187
+ if (args.positional.length) throw new Error(`sync takes no skill names: it installs exactly what skilllib.json pins. To add a skill: skilllib add ${args.positional.join(" ")}`);
3188
+ const roots = changeEach(
3189
+ args,
3190
+ (root) => syncProject(root, { force: args.force }),
3191
+ (root) => existsSync14(join15(root, MANIFEST_FILE)) ? success("Installed versions match skilllib.json") : info(dim("No skilllib.json: nothing to sync"))
3192
+ );
2906
3193
  if (!args.all) rememberProjects(roots);
2907
3194
  }
2908
3195
  function update(args) {
2909
- const roots = args.all ? visibleProjects() : [requireProject()];
2910
- const results = roots.map((root) => {
2911
- if (args.allowTracked) allowTrackedLinks(root);
2912
- const names = args.positional.length ? args.positional : void 0;
2913
- const changes = updateProject(root, names, { force: args.force });
2914
- if (!args.all) {
2915
- const { skills } = readManifest(root);
2916
- for (const name of names ?? []) if (!(name in skills)) changes.push({ name, action: "skipped", reason: "not managed by skilllib in this project" });
2917
- }
2918
- if (changes.some((c) => c.action === "skipped")) process.exitCode = 1;
2919
- if (!args.json) {
2920
- if (args.all) info(`${basename9(root)} ${dim(tildify(root))}`);
2921
- if (changes.length === 0) success("Everything is on the latest version");
2922
- else printChanges(root, changes, args);
2923
- }
2924
- return { project: basename9(root), path: root, changes: withDirs(root, changes) };
2925
- });
2926
- if (args.json) args.all ? json("changes --all", results) : json("changes", results[0].changes);
3196
+ const names = args.positional.length ? args.positional : void 0;
3197
+ changeEach(
3198
+ args,
3199
+ (root) => {
3200
+ const changes = updateProject(root, names, { force: args.force });
3201
+ if (!args.all) {
3202
+ for (const name of names ?? []) if (!(name in readManifest(root).skills)) changes.push({ name, action: "skipped", reason: "not managed by skilllib in this project" });
3203
+ }
3204
+ return changes;
3205
+ },
3206
+ () => success("Everything is on the latest version")
3207
+ );
2927
3208
  }
2928
3209
  function outdated(args) {
2929
- const roots = args.all ? visibleProjects() : [requireProject()];
3210
+ const roots = projectsFor(args);
2930
3211
  const behind = roots.flatMap(
2931
- (root) => projectStatus(root).filter((s) => s.managed && s.latest !== null && s.version !== null && s.latest > s.version).map((s) => ({ project: basename9(root), path: root, skill: s.name, installed: s.version, latest: s.latest, state: s.state }))
3212
+ (root) => projectStatus(root).filter((s) => s.state.endsWith("update available") && s.latest !== null && s.version !== null).map((s) => ({ project: basename9(root), path: root, skill: s.name, installed: s.version, latest: s.latest, state: s.state }))
2932
3213
  );
2933
3214
  if (args.json) return json("outdated", behind);
2934
- const rows = behind.map((b) => ({ Project: b.project, Skill: b.skill, Installed: `v${b.installed}`, Latest: `v${b.latest}`, Status: colorState(b.state) }));
2935
- if (rows.length === 0) return success("Everything is on the latest version");
2936
- table(rows);
3215
+ const rows2 = behind.map((b) => ({ Project: b.project, Skill: b.skill, Installed: `v${b.installed}`, Latest: `v${b.latest}`, Status: colorState(b.state) }));
3216
+ if (rows2.length === 0) return success("Everything is on the latest version");
3217
+ table(rows2);
2937
3218
  info(dim(`
2938
3219
  \u2192 skilllib update${args.all ? " --all" : ""}`));
2939
3220
  }
2940
3221
  function importSkills(args) {
2941
- const dirs = args.global ? machineSkills().filter((m) => m.movable && !m.broken).map((m) => m.path) : args.positional.map((p) => resolve7(p));
3222
+ if (args.global && args.positional.length) throw new Error("skilllib import --global takes no folders: it imports all your global skills");
3223
+ const dirs = args.global ? yourGlobals().map((m) => m.path) : args.positional.map((p) => resolve7(p));
2942
3224
  if (dirs.length === 0) {
2943
3225
  throw new Error("Usage: skilllib import <skill-dir>... [--force] | skilllib import --global");
2944
3226
  }
@@ -2958,14 +3240,11 @@ function importSkills(args) {
2958
3240
  continue;
2959
3241
  }
2960
3242
  success(`${name} ${dim(result === "unchanged" ? "already in library" : result === "added" ? "added to library" : "library updated")}`);
2961
- const skillsDir = resolve7(dir, "..");
2962
- const root = resolve7(skillsDir, "..", "..");
2963
- if (skillsDir === join14(root, ".claude", "skills") && isProjectCandidate(root)) {
2964
- if (readManifest(root).skills[name]?.hash !== treeHash(dir)) {
2965
- const change = addSkill(root, name);
2966
- if (change.action !== "skipped") info(dim(` ${basename9(root)} now depends on ${name} v${change.to}`));
2967
- }
2968
- rememberProjects([root]);
3243
+ const root = resolve7(dir, "..", "..", "..");
3244
+ const dep = projectHere(root) === root ? readManifest(root).skills[name] : void 0;
3245
+ if (dep && realpathOrNull(join15(root, dep.dir ?? PROJECT_SKILLS_DIR, name)) === realpathOrNull(dir) && dep.hash !== treeHash(dir)) {
3246
+ const change = addSkill(root, name);
3247
+ if (change.action !== "skipped") info(dim(` ${basename9(root)} now depends on ${name} v${change.to}`));
2969
3248
  }
2970
3249
  }
2971
3250
  if (args.json) json("import", results);
@@ -2980,7 +3259,7 @@ function scan(args) {
2980
3259
  const found = discoverProjects();
2981
3260
  const library = new Set(librarySkills().map((s) => s.name));
2982
3261
  const skillNames = (root) => [
2983
- .../* @__PURE__ */ new Set([...ALL_PROJECT_DIRS.flatMap((d) => skillDirsIn(join14(root, d))).map((d) => basename9(d)), ...nestedSkills(root).map((s) => s.name)])
3262
+ .../* @__PURE__ */ new Set([...ALL_PROJECT_DIRS.flatMap((d) => skillDirsIn(join15(root, d))).map((d) => basename9(d)), ...nestedSkills(root).map((s) => s.name)])
2984
3263
  ];
2985
3264
  const withSkills = found.filter((root) => skillNames(root).length > 0);
2986
3265
  info(`Scanned ${roots.map((r) => tildify(r)).join(", ")}: ${found.length} project${found.length === 1 ? "" : "s"}, ${withSkills.length} with skills
@@ -2997,6 +3276,7 @@ ${yellow("Yellow")} skills aren't in your library yet (\`skilllib import <skill-
2997
3276
  }
2998
3277
  function folders(args) {
2999
3278
  const [action, ...paths] = args.positional;
3279
+ if (action && !["add", "remove", "rm"].includes(action)) throw new Error("Usage: skilllib folders [add|remove <folder>...]");
3000
3280
  let failed = false;
3001
3281
  if (action === "add") {
3002
3282
  for (const p of paths) {
@@ -3011,7 +3291,10 @@ function folders(args) {
3011
3291
  } else if (action === "remove" || action === "rm") {
3012
3292
  for (const p of paths) {
3013
3293
  if (removeRoot(p)) success(`Removed ${tildify(expandHome(p))}`);
3014
- else warn(`${tildify(expandHome(p))} wasn't a project folder`);
3294
+ else {
3295
+ warn(`${tildify(expandHome(p))} wasn't a project folder`);
3296
+ failed = true;
3297
+ }
3015
3298
  }
3016
3299
  }
3017
3300
  const { roots, hidden } = readConfig();
@@ -3045,7 +3328,7 @@ function newSkill(args) {
3045
3328
  if (!r.ok) {
3046
3329
  throw new Error(`Can't create ${name}: ${r.reason}`);
3047
3330
  }
3048
- success(`Created ${tildify(join14(r.dir, "SKILL.md"))}`);
3331
+ success(`Created ${tildify(join15(r.dir, "SKILL.md"))}`);
3049
3332
  }
3050
3333
  function restore(args) {
3051
3334
  const backups = [...listBackups(), ...pluginBackups()];
@@ -3056,15 +3339,17 @@ function restore(args) {
3056
3339
  info(dim("\n\u2192 skilllib restore <name>"));
3057
3340
  return;
3058
3341
  }
3059
- const backup = backups.find((b) => b.name === name);
3060
- if (!backup) {
3061
- throw new Error(`No backup named ${name}`);
3062
- }
3063
- const r = backup.kind === "plugin" ? restorePlugin(backup) : restoreBackup(backup);
3064
- if (r.ok) success(`Restored ${name} to ${tildify(r.to)}`);
3065
- else {
3066
- error(`Can't restore ${name}: ${r.reason}`);
3067
- process.exitCode = 1;
3342
+ const newest = backups.find((b) => b.name === name);
3343
+ if (!newest) throw new Error(`No backup named ${name} (skilllib restore lists them; a skill removed from a repo that your library has comes back with skilllib add ${name})`);
3344
+ const at = (b) => Date.parse(b.path.slice(-24).replace(/^(.{13})-(\d\d)-(\d\d)-(\d{3})Z$/, "$1:$2:$3.$4Z"));
3345
+ const together = newest.kind === "plugin" ? [newest] : backups.filter((b) => b.name === name && b.kind === newest.kind && Math.abs(at(b) - at(newest)) <= 2e3);
3346
+ for (const backup of together) {
3347
+ const r = backup.kind === "plugin" ? restorePlugin(backup) : restoreBackup(backup);
3348
+ if (r.ok) success(`Restored ${name} to ${tildify(r.to)}`);
3349
+ else {
3350
+ error(`Can't restore ${name}: ${r.reason}`);
3351
+ process.exitCode = 1;
3352
+ }
3068
3353
  }
3069
3354
  }
3070
3355
  async function allIssues(args, withUsage = true) {
@@ -3076,11 +3361,14 @@ async function allIssues(args, withUsage = true) {
3076
3361
  const projectOf = projectOfFactory(projects2);
3077
3362
  const uses = await scanUsage(args.days);
3078
3363
  const inProject = usesByProject(uses, projectOf);
3079
- const total = new Map(summarize(uses, projectOf).map((s) => [s.skill, s.uses]));
3364
+ const total = new Map(summarize2(uses, projectOf).map((s) => [s.skill, s.uses]));
3365
+ const asUsed = /* @__PURE__ */ new Map();
3366
+ for (const u of uses) asUsed.set(u.skill, (asUsed.get(u.skill) ?? 0) + 1);
3080
3367
  issues.push(
3081
- ...usageIssues(projects2, machine, libraryNames, {
3368
+ ...usageIssues(readableRoots(projects2).readable, machine, libraryNames, {
3082
3369
  inProject: (root, skill) => inProject.get(root)?.get(skill) ?? 0,
3083
3370
  total: (skill) => total.get(skill) ?? 0,
3371
+ asUsed: (key) => asUsed.get(key) ?? 0,
3084
3372
  days: args.days
3085
3373
  })
3086
3374
  );
@@ -3125,7 +3413,7 @@ async function doctor(args) {
3125
3413
  async function apply(args) {
3126
3414
  const [id, pick] = args.positional;
3127
3415
  if (!id) throw new Error("Usage: skilllib apply <issue-id> [choice] (ids and choices: skilllib status --json or doctor --json)");
3128
- const found = [...statusIssues(findProjectRoot()), ...await allIssues(args, id.startsWith("unused:"))].filter((i) => i.id === id);
3416
+ const found = [...statusIssues(findProjectRoot()), ...await allIssues(args, /^(plugin-)?unused:/.test(id))].filter((i) => i.id === id);
3129
3417
  if (!found.length) throw new Error(`No issue ${id} right now (it may be fixed already; for an unused:\u2026 issue, pass doctor's --days). See skilllib status --json or doctor --json`);
3130
3418
  const optionsOf = (i) => i.choices ?? (i.fix ? [i.fix] : []);
3131
3419
  const labels = (i) => optionsOf(i).map((c) => c.label).join("\0");
@@ -3148,22 +3436,22 @@ async function apply(args) {
3148
3436
  else error(r.message);
3149
3437
  }
3150
3438
  async function usage(args) {
3151
- const summaries = summarize(await scanUsage(args.days), projectOfFactory(knownProjects()));
3439
+ const summaries = summarize2(await scanUsage(args.days), projectOfFactory(knownProjects()));
3440
+ const library = new Set(librarySkills().map((s) => s.name));
3441
+ const unused = [...library].filter((name) => !summaries.some((u) => u.skill === name));
3152
3442
  if (args.json) {
3153
- const library2 = new Set(librarySkills().map((s) => s.name));
3154
3443
  return json("usage", {
3155
3444
  usageDays: args.days,
3156
3445
  source: "Claude Code transcripts",
3157
- skills: summaries.map((s) => ({ skill: s.skill, uses: s.uses, lastUsed: s.lastUsed, projects: [...s.projects].map((p) => basename9(p)), inLibrary: library2.has(s.skill) })),
3158
- unusedLibrarySkills: librarySkills().filter((l) => !summaries.some((u) => u.skill === l.name)).map((l) => l.name)
3446
+ skills: summaries.map((s) => ({ skill: s.skill, uses: s.uses, lastUsed: s.lastUsed, projects: [...s.projects].map((p) => basename9(p)), inLibrary: library.has(s.skill) })),
3447
+ unusedLibrarySkills: unused
3159
3448
  });
3160
3449
  }
3161
3450
  if (summaries.length === 0) {
3162
3451
  info(`No skill usage in Claude Code transcripts from the last ${args.days} days.`);
3163
3452
  return;
3164
3453
  }
3165
- const library = new Set(librarySkills().map((s) => s.name));
3166
- info(`Skill usage, last ${args.days} days ${dim(`(from ${tildify(join14(claudeDir(), "projects"))})`)}
3454
+ info(`Skill usage, last ${args.days} days ${dim(`(from ${tildify(join15(claudeDir(), "projects"))})`)}
3167
3455
  `);
3168
3456
  table(
3169
3457
  summaries.map((s) => ({
@@ -3174,15 +3462,14 @@ async function usage(args) {
3174
3462
  Library: library.has(s.skill) ? green("yes") : dim("no")
3175
3463
  }))
3176
3464
  );
3177
- const unused = librarySkills().filter((s) => !summaries.some((u) => u.skill === s.name));
3178
3465
  if (unused.length > 0) {
3179
3466
  info("");
3180
- warn(`Library skills unused in the last ${args.days} days: ${unused.map((s) => s.name).join(", ")}`);
3467
+ warn(`Library skills unused in the last ${args.days} days: ${unused.join(", ")}`);
3181
3468
  }
3182
3469
  }
3183
3470
  function show(args) {
3184
3471
  const name = args.positional[0];
3185
- const dir = name ? join14(libraryDir(), name) : "";
3472
+ const dir = name ? join15(libraryDir(), name) : "";
3186
3473
  if (!name || !isSkillDir(dir)) {
3187
3474
  throw new Error(name ? `${name} is not in the library` : "Usage: skilllib show <skill-name>");
3188
3475
  }
@@ -3194,14 +3481,14 @@ function show(args) {
3194
3481
  path: dir,
3195
3482
  latest: latestVersion(name)?.version ?? null,
3196
3483
  origin: libraryOrigins()[name] ?? "",
3197
- projects: knownProjects().filter((p) => name in readManifest(p).skills).map((p) => basename9(p)),
3198
- skillMd: readFileSync9(join14(dir, "SKILL.md"), "utf-8")
3484
+ projects: knownProjects().filter((p) => name in pinsIn(p)).map((p) => basename9(p)),
3485
+ skillMd: readFileSync6(join15(dir, "SKILL.md"), "utf-8")
3199
3486
  });
3200
3487
  }
3201
3488
  info(`${name} ${dim(tildify(dir))}`);
3202
3489
  if (description) info(`
3203
3490
  ${description}`);
3204
- const using = knownProjects().filter((p) => name in readManifest(p).skills);
3491
+ const using = knownProjects().filter((p) => name in pinsIn(p));
3205
3492
  info(`
3206
3493
  Installed in: ${using.map((p) => tildify(p)).join(", ") || "no projects"}`);
3207
3494
  }
@@ -3223,7 +3510,7 @@ function harnessesCommand(args) {
3223
3510
  New installs go to ${installDirs(enabled).join(" (real copy) + ")}${installDirs(enabled).length > 1 ? " (link)" : ""}.`));
3224
3511
  }
3225
3512
  function link(args) {
3226
- const roots = args.all ? visibleProjects() : [requireProject()];
3513
+ const roots = projectsFor(args);
3227
3514
  for (const root of roots) {
3228
3515
  if (args.allowTracked) allowTrackedLinks(root);
3229
3516
  const res = linkAll(root);
@@ -3234,6 +3521,7 @@ function link(args) {
3234
3521
  process.exitCode = 1;
3235
3522
  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`);
3236
3523
  }
3524
+ if (res.uncommitted.length) process.exitCode = 1;
3237
3525
  if (res.uncommitted.length)
3238
3526
  warn(
3239
3527
  `${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`
@@ -3242,7 +3530,7 @@ function link(args) {
3242
3530
  }
3243
3531
  function globalCommand(args) {
3244
3532
  const [action, ...names] = args.positional;
3245
- const yours = machineSkills().filter((m) => m.movable && !m.broken);
3533
+ const yours = yourGlobals();
3246
3534
  if (action === "move") return moveGlobal(args, names, yours);
3247
3535
  if (action === "keep" || action === "unkeep") {
3248
3536
  const unknown = names.filter((n) => !yours.some((m) => m.name === n));
@@ -3267,7 +3555,7 @@ function repoRoot(repo) {
3267
3555
  if (named.length === 1) return named[0];
3268
3556
  if (named.length > 1) throw new Error(`Several projects are named ${repo}: ${named.map(tildify).join(", ")}. Pass its path.`);
3269
3557
  const path = expandHome(repo);
3270
- if (!existsSync13(path)) throw new Error(`No project ${repo} (pass its path, or see skilllib projects)`);
3558
+ if (!existsSync14(path)) throw new Error(`No project ${repo} (pass its path, or see skilllib projects)`);
3271
3559
  const root = projectHere(path);
3272
3560
  if (root !== path) throw new Error(root ? `${tildify(path)} is inside ${tildify(root)}: pass the repo's top folder` : `${tildify(path)} isn't in a git repo`);
3273
3561
  return root;
@@ -3279,6 +3567,7 @@ function moveGlobal(args, names, yours) {
3279
3567
  const roots = [...new Set(args.to.map(repoRoot))];
3280
3568
  const skills = yours.filter((m) => names.includes(m.name));
3281
3569
  const moves = moveGlobalSkills(skills, roots, { allowTracked: args.allowTracked });
3570
+ rememberProjects(roots);
3282
3571
  if (moves.some((m) => !m.moved)) process.exitCode = 1;
3283
3572
  if (args.json) {
3284
3573
  return json(
@@ -3294,10 +3583,7 @@ function moveGlobal(args, names, yours) {
3294
3583
  for (const m of moves) {
3295
3584
  if (m.moved) success(`${m.name} now loads only in ${m.changes.map(([r]) => basename9(r)).join(", ")} (the global copy is in skilllib restore)`);
3296
3585
  else error(`${m.name}: ${m.reason}`);
3297
- for (const [root, c] of m.changes) {
3298
- if (c.blocked?.length) warn(` ${basename9(root)}: git tracks ${c.blocked.join(", ")}, so no link there. Re-run with --allow-tracked to add it.`);
3299
- if (c.uncommitted?.length) warn(` ${basename9(root)}: no link in ${c.uncommitted.join(", ")}: git tracks it but not the skill's copy, so the link would break for teammates. Commit the copy, then run skilllib link --allow-tracked.`);
3300
- }
3586
+ for (const [root, c] of m.changes) warnHeldLinks(c, `in ${basename9(root)}`);
3301
3587
  }
3302
3588
  if (moves.some((m) => m.moved)) info(dim("Commit skilllib.json and the skill folders in each repo."));
3303
3589
  }
@@ -3307,7 +3593,7 @@ function tidy(args) {
3307
3593
  if (args.keep && !names.length) {
3308
3594
  throw new Error("Usage: skilllib tidy <name>... --keep <folder>");
3309
3595
  }
3310
- const targets = args.global ? [null] : args.all ? visibleProjects() : [requireProject()];
3596
+ const targets = args.global ? [null] : projectsFor(args);
3311
3597
  const problems = [];
3312
3598
  for (const name of names) {
3313
3599
  const where = targets.map((root) => ({ root, ...foldersOf(root, name) })).filter((w) => w.all.length);
@@ -3335,20 +3621,20 @@ function tidy(args) {
3335
3621
  for (const { label, report } of reports) {
3336
3622
  const plans = report.plans.filter((p) => !names.length || names.includes(p.name));
3337
3623
  const conflicts = report.conflicts.filter((c) => !names.length || names.includes(c.name));
3338
- if (!plans.length && !conflicts.length && !args.all) success(`${label}: every skill has one copy and the links your agents need`);
3624
+ if (!plans.length && !conflicts.length && !args.all) success(`${label}: nothing to tidy (no extra copies or links)`);
3339
3625
  for (const plan of plans) {
3340
- const result = args.dryRun ? { applied: [], held: gitVisibleSteps(plan) } : applyTidy(plan, { git: args.allowGit ? "go" : "keep" });
3341
- const show2 = args.dryRun ? plan.steps : result.applied;
3342
- if (show2.length) info(`${args.dryRun ? dim("would") : green("\u2713")} ${label} \xB7 ${plan.name}: ${show2.map((s) => describeStep(s, plan.root)).join(", ")}`);
3343
- if (!args.dryRun) for (const step of result.held) info(`${yellow("!")} ${label} \xB7 ${plan.name}: held back, git would see it: ${describeStep(step, plan.root)}`);
3344
- held += args.dryRun ? 0 : result.held.length;
3626
+ const wouldHold = args.allowGit ? [] : gitVisibleSteps(plan);
3627
+ const result = args.dryRun ? { applied: plan.steps.filter((s) => !wouldHold.includes(s)), held: wouldHold } : applyTidy(plan, { git: args.allowGit ? "go" : "keep" });
3628
+ if (result.applied.length) info(`${args.dryRun ? dim("would") : green("\u2713")} ${label} \xB7 ${plan.name}: ${result.applied.map((s) => describeStep(s, plan.root)).join(", ")}`);
3629
+ for (const step of result.held) info(`${yellow("!")} ${label} \xB7 ${plan.name}: ${args.dryRun ? "would be held" : "held"} back, git would see it: ${describeStep(step, plan.root)}`);
3630
+ held += result.held.length;
3345
3631
  changed += result.applied.length;
3346
3632
  }
3347
3633
  if (args.keep && conflicts.length) process.exitCode = 1;
3348
3634
  for (const c of conflicts) {
3349
3635
  warn(`${label} \xB7 ${c.name}: the copies differ`);
3350
3636
  for (const copy of c.copies) {
3351
- const runs = copy.runs.map((id) => HARNESSES.find((h) => h.id === id).name);
3637
+ const runs = copy.runs.map((id) => harness(id).name);
3352
3638
  info(` ${copyLabel(c, copy.dir)}${runs.length ? dim(` run by ${runs.join(", ")}`) : ""}${c.managed === copy.dir ? dim(" (skilllib's copy)") : ""}`);
3353
3639
  }
3354
3640
  const pick = copyLabel(c, c.managed ?? c.copies[0].dir);
@@ -3378,6 +3664,7 @@ function agentSkill(args) {
3378
3664
  if (args.json) return json("agent-skill", { state: agentSkillState(), removed });
3379
3665
  return removed.length ? success(`Removed from ${removed.map((d) => tildify(d)).join(" and ")}`) : info("The skilllib skill isn't installed.");
3380
3666
  }
3667
+ if (action) throw new Error("Usage: skilllib agent-skill [install|remove]");
3381
3668
  const state = agentSkillState();
3382
3669
  if (args.json) return json("agent-skill", { state, dirs: agentSkillDirs() });
3383
3670
  if (state === "installed") success(`Your agents can use skilllib ${dim(`(${agentSkillDirs().map((d) => tildify(d)).join(", ")})`)}`);
@@ -3391,16 +3678,19 @@ export {
3391
3678
  HARNESSES,
3392
3679
  harness,
3393
3680
  readSkillInfo,
3681
+ treeHash,
3394
3682
  versionHistory,
3395
3683
  forgetLatest,
3396
3684
  latestVersion,
3397
3685
  projectHere,
3398
3686
  readManifest,
3687
+ readableRoots,
3399
3688
  setJsonMode,
3400
3689
  isJsonMode,
3401
3690
  json,
3402
3691
  error,
3403
3692
  tildify,
3693
+ localStamp,
3404
3694
  readConfig,
3405
3695
  addRoot,
3406
3696
  removeRoot,
@@ -3425,43 +3715,34 @@ export {
3425
3715
  relinkDependency,
3426
3716
  addSkill,
3427
3717
  removeSkill,
3428
- syncSkill,
3429
3718
  updateProject,
3430
3719
  importSkill,
3431
3720
  removeUntracked,
3432
3721
  projectsUsing,
3433
3722
  deleteLibrarySkill,
3434
3723
  deleteGlobal,
3435
- unloadGlobal,
3436
- linkGlobal,
3437
3724
  createSkill,
3438
3725
  listBackups,
3439
3726
  restoreBackup,
3440
3727
  gitInfo,
3441
3728
  relativeTo,
3442
- claudePlugins,
3443
3729
  installedPlugins,
3444
3730
  cursorPlugins,
3445
3731
  forgetClaudeBinary,
3446
3732
  claudeBinary,
3447
- turnOffIn,
3448
3733
  setPluginOn,
3449
3734
  uninstallPlugin,
3450
3735
  updatePlugin,
3451
3736
  pluginBackups,
3452
3737
  restorePlugin,
3453
- cursorPluginSkills,
3454
3738
  agentSkillState,
3455
3739
  installAgentSkill,
3456
3740
  removeAgentSkill,
3457
3741
  refreshAgentSkill,
3458
- planProjectTidy,
3459
- planGlobalTidy,
3460
- gitVisibleSteps,
3461
- applyTidy,
3462
- describeStep,
3463
- copyLabel,
3464
- addedAt,
3742
+ foldersOf,
3743
+ runFix,
3744
+ findIssues,
3745
+ usageIssues,
3465
3746
  scanUsage,
3466
3747
  moveGlobalSkills,
3467
3748
  parseArgs,