skilllib 1.11.1 → 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,12 +41,53 @@ 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;
47
87
  function setJsonMode(on) {
48
88
  jsonMode = on;
49
89
  }
90
+ var isJsonMode = () => jsonMode;
50
91
  function json(output, data) {
51
92
  process.stdout.write(JSON.stringify(data) + "\n");
52
93
  }
@@ -70,21 +111,25 @@ function truncate(s, max) {
70
111
  return line.length > max ? line.slice(0, max - 1) + "\u2026" : line;
71
112
  }
72
113
  var visible = (s) => s.replace(/\x1b\[\d+m/g, "");
73
- function table(rows) {
74
- if (rows.length === 0) return;
75
- const keys = Object.keys(rows[0]);
76
- 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)));
77
118
  const pad = (s, w) => s + " ".repeat(Math.max(0, w - visible(s).length));
78
119
  info(dim(keys.map((k, i) => pad(k, widths[i])).join(" ")));
79
- 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", " ");
80
125
  }
81
126
 
82
127
  // src/agentSkill.ts
83
- 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";
84
129
  import { join as join9 } from "path";
85
130
 
86
131
  // src/config.ts
87
- 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";
88
133
  import { homedir as homedir2 } from "os";
89
134
  import { join as join6, resolve as resolve4 } from "path";
90
135
 
@@ -184,16 +229,16 @@ function installDirs(enabled, keep) {
184
229
  }
185
230
 
186
231
  // src/project.ts
187
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
188
- 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";
189
234
 
190
235
  // src/versions.ts
191
- import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
236
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
192
237
  import { join as join4 } from "path";
193
238
 
194
239
  // src/skills.ts
195
240
  import { createHash } from "crypto";
196
- 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";
197
242
  import { basename, join as join3, relative, sep as sep2 } from "path";
198
243
  import { parse as parseYaml } from "yaml";
199
244
  var IGNORED = /* @__PURE__ */ new Set([".DS_Store", ".git", "node_modules"]);
@@ -201,7 +246,7 @@ function readSkillInfo(dir) {
201
246
  const skillMd = join3(dir, "SKILL.md");
202
247
  const fallback = { name: basename(dir), description: "" };
203
248
  if (!existsSync2(skillMd)) return fallback;
204
- 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---/);
205
250
  if (!match) return fallback;
206
251
  try {
207
252
  const fm = parseYaml(match[1]);
@@ -219,21 +264,23 @@ function isSkillDir(dir) {
219
264
  function listFiles(dir) {
220
265
  return readdirSync(dir).filter((entry) => !IGNORED.has(entry)).flatMap((entry) => {
221
266
  const full = join3(dir, entry);
222
- return statSync(full).isDirectory() ? listFiles(full) : [full];
267
+ const st = statSync2(full, { throwIfNoEntry: false });
268
+ return !st ? [] : st.isDirectory() ? listFiles(full) : [full];
223
269
  });
224
270
  }
225
271
  function treeHash(dir) {
226
272
  if (!existsSync2(dir)) return null;
227
273
  const hash = createHash("sha256");
228
- 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"));
229
275
  for (const file of files) {
230
- 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");
231
277
  }
232
278
  return hash.digest("hex").slice(0, 16);
233
279
  }
234
280
  function copySkill(from, to) {
235
- rmSync(to, { recursive: true, force: true });
236
- cpSync(realpathSync2(from), to, { recursive: true, dereference: true, filter: (src) => !IGNORED.has(basename(src)) });
281
+ const source = realpathSync2(from);
282
+ rmSync2(to, { recursive: true, force: true });
283
+ cpSync(source, to, { recursive: true, dereference: true, filter: (src) => !IGNORED.has(basename(src)) && existsSync2(src) });
237
284
  }
238
285
  function skillDirsIn(dir) {
239
286
  if (!existsSync2(dir)) return [];
@@ -248,11 +295,7 @@ function indexFile(name) {
248
295
  return join4(storeDir(name), "versions.json");
249
296
  }
250
297
  function versionHistory(name) {
251
- try {
252
- return JSON.parse(readFileSync2(indexFile(name), "utf-8"));
253
- } catch {
254
- return [];
255
- }
298
+ return readState(indexFile(name)) ?? [];
256
299
  }
257
300
  function versionDir(name, version) {
258
301
  return join4(storeDir(name), String(version));
@@ -274,9 +317,10 @@ function latestVersion(name) {
274
317
  result = last;
275
318
  } else {
276
319
  const next = { version: (last?.version ?? 0) + 1, hash, date: (/* @__PURE__ */ new Date()).toISOString() };
277
- 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 });
278
322
  copySkill(source, versionDir(name, next.version));
279
- writeFileSync(indexFile(name), JSON.stringify([...history, next], null, 2) + "\n");
323
+ writeState(indexFile(name), [...history, next]);
280
324
  result = next;
281
325
  }
282
326
  }
@@ -295,7 +339,7 @@ function findProjectRoot(from = process.cwd()) {
295
339
  let dir = resolve3(from);
296
340
  while (true) {
297
341
  if (existsSync4(join5(dir, MANIFEST_FILE)) || existsSync4(join5(dir, ".git"))) return dir;
298
- const parent = dirname(dir);
342
+ const parent = dirname2(dir);
299
343
  if (parent === dir) return resolve3(from);
300
344
  dir = parent;
301
345
  }
@@ -307,7 +351,12 @@ function projectHere(from = process.cwd()) {
307
351
  function readManifest(root) {
308
352
  const path = join5(root, MANIFEST_FILE);
309
353
  if (!existsSync4(path)) return { skills: {} };
310
- 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
+ }
311
360
  const skills = Object.fromEntries(
312
361
  Object.entries(data.skills ?? {}).flatMap(([name, value]) => {
313
362
  if (name.startsWith("@")) return [];
@@ -332,8 +381,8 @@ function readManifest(root) {
332
381
  return { ...data, skills };
333
382
  }
334
383
  function writeManifest(root, manifest) {
335
- const skills = Object.fromEntries(Object.entries(manifest.skills).sort(([a], [b]) => a.localeCompare(b)));
336
- 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 });
337
386
  }
338
387
  function projectsFile() {
339
388
  return join5(skilllibHome(), "projects.json");
@@ -341,7 +390,7 @@ function projectsFile() {
341
390
  function knownProjects() {
342
391
  const path = projectsFile();
343
392
  if (!existsSync4(path)) return [];
344
- const list2 = JSON.parse(readFileSync3(path, "utf-8"));
393
+ const list2 = readState(path) ?? [];
345
394
  return onePerFolder(list2.filter((p) => existsSync4(p) && isProjectCandidate(p) && !isAgentWorktree(p)));
346
395
  }
347
396
  function isAgentWorktree(path) {
@@ -362,8 +411,27 @@ function isProjectCandidate(root) {
362
411
  function rememberProjects(roots) {
363
412
  const incoming = roots.map((r) => resolve3(r)).filter((r) => isProjectCandidate(r) && !isAgentWorktree(r));
364
413
  const all = onePerFolder([...knownProjects(), ...incoming]).sort();
365
- mkdirSync2(skilllibHome(), { recursive: true });
366
- 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
+ }
367
435
  }
368
436
 
369
437
  // src/config.ts
@@ -371,16 +439,11 @@ function configFile() {
371
439
  return join6(skilllibHome(), "config.json");
372
440
  }
373
441
  function readConfig() {
374
- try {
375
- const data = JSON.parse(readFileSync4(configFile(), "utf-8"));
376
- return { ...data, roots: data.roots ?? [], hidden: data.hidden ?? [] };
377
- } catch {
378
- return { roots: [], hidden: [] };
379
- }
442
+ const data = readState(configFile()) ?? {};
443
+ return { ...data, roots: data.roots ?? [], hidden: data.hidden ?? [] };
380
444
  }
381
445
  function writeConfig(config) {
382
- mkdirSync3(skilllibHome(), { recursive: true });
383
- writeFileSync3(configFile(), JSON.stringify(config, null, 2) + "\n");
446
+ writeState(configFile(), config);
384
447
  }
385
448
  function expandHome(path) {
386
449
  return resolve4(path.replace(/^~(?=$|\/)/, homedir2()));
@@ -388,7 +451,7 @@ function expandHome(path) {
388
451
  function addRoot(path) {
389
452
  const root = expandHome(path);
390
453
  if (!existsSync5(root)) throw new Error(`${tildify(root)} doesn't exist`);
391
- 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`);
392
455
  const config = readConfig();
393
456
  if (!config.roots.includes(root)) writeConfig({ ...config, roots: [...config.roots, root].sort() });
394
457
  return root;
@@ -461,35 +524,25 @@ function setKeepGlobal(names, keep) {
461
524
 
462
525
  // src/library.ts
463
526
  import { execFileSync } from "child_process";
464
- import { existsSync as existsSync7, lstatSync as lstatSync2, mkdirSync as mkdirSync5, readdirSync as readdirSync4, readFileSync as readFileSync6, realpathSync as realpathSync4, renameSync, rmSync as rmSync2, symlinkSync, unlinkSync, writeFileSync as writeFileSync5 } from "fs";
465
- 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";
466
529
 
467
530
  // src/sources.ts
468
- import { existsSync as existsSync6, lstatSync, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync5, readlinkSync, realpathSync as realpathSync3, writeFileSync as writeFileSync4 } from "fs";
469
- import { basename as basename2, join as join7, resolve as resolve5 } from "path";
470
- function readJson(path) {
471
- try {
472
- return JSON.parse(readFileSync5(path, "utf-8"));
473
- } catch {
474
- return null;
475
- }
476
- }
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";
477
534
  function skillsShLock() {
478
535
  return readJson(join7(userHome(), ".agents", ".skill-lock.json"))?.skills ?? {};
479
536
  }
480
537
  function projectSkillsLock(root) {
481
538
  return readJson(join7(root, "skills-lock.json"))?.skills ?? {};
482
539
  }
483
- function realpathOrNull(path) {
484
- try {
485
- return realpathSync3(path);
486
- } catch {
487
- return null;
488
- }
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: [] };
489
542
  }
490
543
  function harnessesReading(dir, enabled) {
491
544
  const real = realPath(dir);
492
- 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));
493
546
  }
494
547
  var systemDirs = () => [codexSystemDir()];
495
548
  function globalSkillDirs() {
@@ -508,17 +561,7 @@ function globalDirAs(dir) {
508
561
  }
509
562
  function systemSkills(enabled) {
510
563
  return systemDirs().flatMap(
511
- (dir) => skillDirsIn(dir).map((path) => ({
512
- name: basename2(path),
513
- kind: "system",
514
- origin: dir.replace(/\\/g, "/"),
515
- path,
516
- description: readSkillInfo(path).description,
517
- movable: false,
518
- broken: false,
519
- harnesses: harnessesReading(dir, enabled),
520
- links: []
521
- }))
564
+ (dir) => skillDirsIn(dir).map((path) => vendorSkill(path, "system", dir.replace(/\\/g, "/"), harnessesReading(dir, enabled)))
522
565
  ).filter((s) => s.harnesses.length > 0);
523
566
  }
524
567
  function globalFolderSkills(enabled) {
@@ -547,7 +590,7 @@ function globalFolderSkills(enabled) {
547
590
  const inAgents = e.dir === agentsDir || target_.includes(".agents/skills");
548
591
  const locked = lock[e.name]?.source;
549
592
  if (existsSync6(join7(e.path, OWN_SKILL_MARKER))) {
550
- 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)));
551
594
  continue;
552
595
  }
553
596
  skills.push({
@@ -575,31 +618,11 @@ function tildify2(p) {
575
618
  }
576
619
  function grokBuiltInSkills(enabled) {
577
620
  if (!enabled.includes("grok")) return [];
578
- return skillDirsIn(join7(grokDir(), "bundled", "skills")).map((path) => ({
579
- name: basename2(path),
580
- kind: "built-in",
581
- origin: "Grok",
582
- path,
583
- description: readSkillInfo(path).description,
584
- movable: false,
585
- broken: false,
586
- harnesses: ["grok"],
587
- links: []
588
- }));
621
+ return skillDirsIn(join7(grokDir(), "bundled", "skills")).map((path) => vendorSkill(path, "built-in", "Grok", ["grok"]));
589
622
  }
590
623
  function cursorBuiltInSkills(enabled) {
591
624
  if (!enabled.includes("cursor")) return [];
592
- return skillDirsIn(join7(userHome(), ".cursor", "skills-cursor")).map((path) => ({
593
- name: basename2(path),
594
- kind: "built-in",
595
- origin: "Cursor",
596
- path,
597
- description: readSkillInfo(path).description,
598
- movable: false,
599
- broken: false,
600
- harnesses: ["cursor"],
601
- links: []
602
- }));
625
+ return skillDirsIn(join7(userHome(), ".cursor", "skills-cursor")).map((path) => vendorSkill(path, "built-in", "Cursor", ["cursor"]));
603
626
  }
604
627
  function claudeAiSkills(enabled) {
605
628
  const byReal = /* @__PURE__ */ new Map();
@@ -614,20 +637,13 @@ function claudeAiSkills(enabled) {
614
637
  }
615
638
  return [...byReal.values()].flatMap(
616
639
  ({ dir, readers }) => readdirSync3(dir).filter((d) => !d.startsWith(".")).flatMap(
617
- (bucket) => skillDirsIn(join7(dir, bucket)).map((path) => ({
618
- name: basename2(path),
619
- kind: "claude.ai",
620
- origin: "claude.ai account",
621
- path,
622
- description: readSkillInfo(path).description,
623
- movable: false,
624
- broken: false,
625
- harnesses: enabled.filter((id) => readers.includes(id)),
626
- links: []
627
- }))
640
+ (bucket) => skillDirsIn(join7(dir, bucket)).map((path) => vendorSkill(path, "claude.ai", "claude.ai account", enabled.filter((id) => readers.includes(id))))
628
641
  )
629
642
  );
630
643
  }
644
+ function installedPluginsJson() {
645
+ return readJson(join7(claudeDir(), "plugins", "installed_plugins.json"))?.plugins ?? {};
646
+ }
631
647
  function pluginDirCandidates(id, installPaths) {
632
648
  const [name = "", marketplace = ""] = id.split("@");
633
649
  const plugins = join7(claudeDir(), "plugins");
@@ -650,7 +666,7 @@ function pluginSkills(harnesses, root) {
650
666
  const cursor = harnesses.includes("cursor");
651
667
  if (!claude2 && !cursor) return [];
652
668
  const plugins = join7(claudeDir(), "plugins");
653
- const installed = readJson(join7(plugins, "installed_plugins.json"))?.plugins ?? {};
669
+ const installed = installedPluginsJson();
654
670
  const user = enabledPlugins();
655
671
  const enabled = root ? enabledPlugins(root) : user;
656
672
  const fromMarketplaces = [.../* @__PURE__ */ new Set([...Object.keys(enabled), ...Object.keys(user)])].filter((id) => claude2 && enabled[id] || cursor && user[id]).flatMap((id) => {
@@ -670,17 +686,7 @@ function pluginSkills(harnesses, root) {
670
686
  return skillDirsIn(join7(root2, "skills")).map((path) => ({ path, origin: `${name}@synced`, loadedBy: ["claude-code"] }));
671
687
  })
672
688
  ) : [];
673
- return [...fromMarketplaces, ...fromSynced].map(({ path, origin, loadedBy }) => ({
674
- name: basename2(path),
675
- kind: "plugin",
676
- origin,
677
- path,
678
- description: readSkillInfo(path).description,
679
- movable: false,
680
- broken: false,
681
- harnesses: loadedBy,
682
- links: []
683
- }));
689
+ return [...fromMarketplaces, ...fromSynced].map(({ path, origin, loadedBy }) => vendorSkill(path, "plugin", origin, loadedBy));
684
690
  }
685
691
  var byKindAndName = (a, b) => a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name);
686
692
  function machineSkills(enabled = enabledHarnesses()) {
@@ -702,15 +708,18 @@ function originsFile() {
702
708
  return join7(skilllibHome(), "origins.json");
703
709
  }
704
710
  function libraryOrigins() {
705
- return readJson(originsFile()) ?? {};
711
+ return readState(originsFile()) ?? {};
706
712
  }
707
713
  function recordOrigin(name, origin) {
708
714
  const origins = libraryOrigins();
709
715
  if (origins[name] === origin) return;
710
- mkdirSync4(skilllibHome(), { recursive: true });
711
- writeFileSync4(originsFile(), JSON.stringify({ ...origins, [name]: origin }, null, 2) + "\n");
716
+ mkdirSync3(skilllibHome(), { recursive: true });
717
+ writeState(originsFile(), { ...origins, [name]: origin });
712
718
  }
713
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 "";
714
723
  const match = machineSkills().find((s) => resolve5(s.path) === resolve5(dir));
715
724
  if (match) return match.kind === "global" ? "global folder" : `${match.kind}: ${match.origin}`;
716
725
  const project = dir.split(/[\\/]\.(?:claude|agents|grok|cursor|codex)[\\/]skills[\\/]/)[0];
@@ -730,13 +739,6 @@ function librarySkills() {
730
739
  function librarySkillDir(name) {
731
740
  return join8(libraryDir(), name);
732
741
  }
733
- function realpathOrNull2(path) {
734
- try {
735
- return realpathSync4(path);
736
- } catch {
737
- return null;
738
- }
739
- }
740
742
  function isLink(path) {
741
743
  try {
742
744
  return lstatSync2(path).isSymbolicLink();
@@ -746,7 +748,7 @@ function isLink(path) {
746
748
  }
747
749
  function linkDir(target, link2) {
748
750
  if (process.platform === "win32") symlinkSync(target, link2, "junction");
749
- else symlinkSync(relative2(dirname2(link2), target), link2);
751
+ else symlinkSync(relative2(dirname3(link2), target), link2);
750
752
  }
751
753
  function entryExists(path) {
752
754
  return isLink(path) || existsSync7(path);
@@ -758,9 +760,9 @@ function visibilityOf(root, name, enabled = enabledHarnesses()) {
758
760
  }));
759
761
  }
760
762
  function dependencyDirs(root, name, dep) {
761
- const wanted = installDirs(enabledHarnesses());
762
763
  const untracked = ALL_PROJECT_DIRS.find((d) => d !== GROK_SKILLS_DIR && existsSync7(join8(root, d, name, "SKILL.md")) && !isLink(join8(root, d, name)));
763
- 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);
764
766
  return [primary, .../* @__PURE__ */ new Set([...dep?.links ?? [], ...wanted.filter((d) => d !== primary)])];
765
767
  }
766
768
  function isGitTracked(root, dir) {
@@ -806,7 +808,7 @@ function projectStatus(root) {
806
808
  }
807
809
  for (const [name, paths] of byName) {
808
810
  const real = paths.find((p) => !isLink(p)) ?? paths[0];
809
- const location = relative2(root, join8(real, "..")).split(sep3).join("/");
811
+ const location = relative2(root, join8(real, "..")).split(sep4).join("/");
810
812
  const source = lock[name]?.source;
811
813
  unmanaged.push({
812
814
  name,
@@ -851,9 +853,9 @@ function nestedSkills(root, { depth = 3, enabled = enabledHarnesses() } = {}) {
851
853
  for (const sub of subs) {
852
854
  if (existsSync7(join8(sub, ".git"))) continue;
853
855
  for (const [skillsDir, readers] of NESTED_READERS) {
854
- const location = relative2(root, join8(sub, skillsDir)).split(sep3).join("/");
856
+ const location = relative2(root, join8(sub, skillsDir)).split(sep4).join("/");
855
857
  for (const path of skillDirsIn(join8(sub, skillsDir))) {
856
- if (isLink(path) && (realpathOrNull2(path) ?? "").startsWith((realpathOrNull2(root) ?? root) + sep3)) continue;
858
+ if (isLink(path) && (realpathOrNull(path) ?? "").startsWith((realpathOrNull(root) ?? root) + sep4)) continue;
857
859
  const name = basename3(path);
858
860
  found.push({
859
861
  name,
@@ -904,7 +906,7 @@ function linkEverywhere(root, name, location, { allowTracked = false, dryRun = f
904
906
  }
905
907
  }
906
908
  if (!dryRun) {
907
- mkdirSync5(join8(root, dir), { recursive: true });
909
+ mkdirSync4(join8(root, dir), { recursive: true });
908
910
  linkDir(join8(root, location, name), link2);
909
911
  }
910
912
  result.created.push(dir);
@@ -912,12 +914,12 @@ function linkEverywhere(root, name, location, { allowTracked = false, dryRun = f
912
914
  return result;
913
915
  }
914
916
  function unlinkEverywhere(root, name, removing) {
915
- const gone = removing ? realpathOrNull2(removing) : null;
917
+ const gone = removing ? realpathOrNull(removing) : null;
916
918
  return ALL_PROJECT_DIRS.filter((dir) => {
917
919
  const link2 = join8(root, dir, name);
918
920
  if (!isLink(link2)) return false;
919
921
  if (dir === GROK_SKILLS_DIR) {
920
- const target = realpathOrNull2(link2);
922
+ const target = realpathOrNull(link2);
921
923
  if (target !== null && target !== gone) return false;
922
924
  }
923
925
  unlinkSync(link2);
@@ -957,6 +959,8 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
957
959
  const to = join8(root, primary, name);
958
960
  const local = isLink(to) && !recorded ? null : treeHash(to);
959
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` };
960
964
  if (local !== null && local !== target.hash && !knownContent && !force) {
961
965
  return {
962
966
  name,
@@ -966,9 +970,9 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
966
970
  }
967
971
  let backedUp;
968
972
  if (local !== target.hash || isLink(to)) {
969
- mkdirSync5(join8(root, primary), { recursive: true });
973
+ mkdirSync4(join8(root, primary), { recursive: true });
970
974
  if (isLink(to)) unlinkSync(to);
971
- else if (local !== null && !knownContent) backedUp = stash(to, "edit-backup");
975
+ else if (local !== null && versionForHash(name, local) === null) backedUp = stash(to, "edit-backup");
972
976
  copySkill(versionDir(name, target.version), to);
973
977
  }
974
978
  const links = [];
@@ -977,7 +981,7 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
977
981
  let shared;
978
982
  for (const dir of linkDirs) {
979
983
  const link2 = join8(root, dir, name);
980
- if (isLink(link2) && realpathOrNull2(link2) === realpathOrNull2(to)) {
984
+ if (isLink(link2) && realpathOrNull(link2) === realpathOrNull(to)) {
981
985
  links.push(dir);
982
986
  continue;
983
987
  }
@@ -992,7 +996,7 @@ function addSkill(root, name, { force = false, version, allowTracked = false } =
992
996
  continue;
993
997
  }
994
998
  }
995
- mkdirSync5(join8(root, dir), { recursive: true });
999
+ mkdirSync4(join8(root, dir), { recursive: true });
996
1000
  linkDir(to, link2);
997
1001
  links.push(dir);
998
1002
  }
@@ -1019,38 +1023,60 @@ function removeSkill(root, name, { force = false } = {}) {
1019
1023
  if (!recorded) return { name, action: "skipped", reason: "not managed by skilllib in this project" };
1020
1024
  const dir = join8(root, recorded.dir ?? PROJECT_SKILLS_DIR, name);
1021
1025
  const local = treeHash(dir);
1022
- 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;
1023
1028
  if (edited && !force) return { name, action: "skipped", reason: "has local edits (use --force to delete anyway)" };
1024
1029
  for (const linkDir2 of recorded.links ?? []) {
1025
1030
  const link2 = join8(root, linkDir2, name);
1026
1031
  if (isLink(link2)) unlinkSync(link2);
1027
1032
  }
1028
- const backedUp = edited ? stash(dir, "edit-backup") : void 0;
1029
- 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 });
1030
1035
  delete manifest.skills[name];
1031
1036
  writeManifest(root, manifest);
1032
1037
  return { name, action: "removed", ...backedUp ? { backedUp } : {} };
1033
1038
  }
1034
1039
  function syncProject(root, { force = false } = {}) {
1035
- const { skills } = readManifest(root);
1036
- return Object.entries(skills).flatMap(([name, dep]) => {
1037
- const local = treeHash(join8(root, dep.dir ?? PROJECT_SKILLS_DIR, name));
1038
- const linksOk = (dep.links ?? []).every((d) => existsSync7(join8(root, d, name, "SKILL.md")));
1039
- if (local === dep.hash && linksOk) return [];
1040
- const pinned = dep.version > 0 && getVersion(name, dep.version) ? dep.version : void 0;
1041
- if (local !== null && local !== dep.hash && !force) return [{ name, action: "skipped", reason: "has local edits (use --force to reset them)" }];
1042
- const change = addSkill(root, name, { force: true, version: pinned });
1043
- 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` }];
1044
- return [{ ...change, action: local === null ? "installed" : change.action }];
1045
- });
1040
+ return Object.keys(readManifest(root).skills).flatMap((name) => syncSkill(root, name, { force }));
1041
+ }
1042
+ function syncSkill(root, name, { force = false } = {}) {
1043
+ const dep = readManifest(root).skills[name];
1044
+ if (!dep) return [];
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));
1048
+ if (local === dep.hash && linksOk) return [];
1049
+ if (local === dep.hash) return [relinkPinned(root, name, dep)];
1050
+ if (local !== null && local !== dep.hash && !force) return [{ name, action: "skipped", reason: "has local edits (use --force to reset them)" }];
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 });
1054
+ return [{ ...change, action: local === null ? "installed" : change.action }];
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
+ }
1068
+ function pinnedVersion(name, dep) {
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`;
1046
1071
  }
1047
1072
  function discardEdits(root, name) {
1048
1073
  const dep = readManifest(root).skills[name];
1049
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 };
1050
1077
  const dir = join8(root, dep.dir ?? PROJECT_SKILLS_DIR, name);
1051
- const pinned = dep.version > 0 && getVersion(name, dep.version) ? dep.version : void 0;
1052
1078
  if (existsSync7(dir) && !isLink(dir)) stash(dir, "edit-backup");
1053
- return addSkill(root, name, { force: true, version: pinned });
1079
+ return addSkill(root, name, { force: true, version });
1054
1080
  }
1055
1081
  function updateProject(root, names, { force = false } = {}) {
1056
1082
  const { skills } = readManifest(root);
@@ -1072,7 +1098,7 @@ function importSkill(dir, { force = false } = {}) {
1072
1098
  const incoming = treeHash(dir);
1073
1099
  if (existing === incoming) return { name, status: "unchanged" };
1074
1100
  if (existing !== null && !force) return { name, status: "exists" };
1075
- mkdirSync5(libraryDir(), { recursive: true });
1101
+ mkdirSync4(libraryDir(), { recursive: true });
1076
1102
  const origin = existing !== null && libraryOrigins()[name] ? null : originFor(dir);
1077
1103
  copySkill(dir, to);
1078
1104
  if (origin) recordOrigin(name, origin);
@@ -1084,10 +1110,15 @@ function backupIndexFile(bucket) {
1084
1110
  return join8(skilllibHome(), bucket, "index.json");
1085
1111
  }
1086
1112
  function readBackupIndex(bucket) {
1113
+ return readState(backupIndexFile(bucket)) ?? {};
1114
+ }
1115
+ function move(from, to) {
1087
1116
  try {
1088
- return JSON.parse(readFileSync6(backupIndexFile(bucket), "utf-8"));
1089
- } catch {
1090
- return {};
1117
+ renameSync2(from, to);
1118
+ } catch (err) {
1119
+ if (err.code !== "EXDEV") throw err;
1120
+ cpSync2(from, to, { recursive: true, verbatimSymlinks: true });
1121
+ rmSync3(from, { recursive: true, force: true });
1091
1122
  }
1092
1123
  }
1093
1124
  function stash(dir, bucket) {
@@ -1096,9 +1127,10 @@ function stash(dir, bucket) {
1096
1127
  while (entryExists(join8(skilllibHome(), bucket, entryAt(at)))) at++;
1097
1128
  const entry = entryAt(at);
1098
1129
  const target = join8(skilllibHome(), bucket, entry);
1099
- mkdirSync5(join8(skilllibHome(), bucket), { recursive: true });
1100
- renameSync(dir, target);
1101
- writeFileSync5(backupIndexFile(bucket), JSON.stringify({ ...readBackupIndex(bucket), [entry]: dir }, null, 2) + "\n");
1130
+ const index = readBackupIndex(bucket);
1131
+ mkdirSync4(join8(skilllibHome(), bucket), { recursive: true });
1132
+ move(dir, target);
1133
+ writeState(backupIndexFile(bucket), { ...index, [entry]: dir });
1102
1134
  return target;
1103
1135
  }
1104
1136
  function removeUntracked(root, name, path) {
@@ -1106,7 +1138,7 @@ function removeUntracked(root, name, path) {
1106
1138
  return stash(path, "trash");
1107
1139
  }
1108
1140
  function projectsUsing(name) {
1109
- return knownProjects().filter((p) => name in readManifest(p).skills);
1141
+ return knownProjects().filter((p) => name in pinsIn(p));
1110
1142
  }
1111
1143
  function deleteLibrarySkill(name) {
1112
1144
  const using = projectsUsing(name);
@@ -1135,12 +1167,12 @@ function linkGlobal(path, agents) {
1135
1167
  const skipped = [];
1136
1168
  for (const dir of new Set(agents.map((id) => harness(id).globalDirs()[0]))) {
1137
1169
  const link2 = join8(dir, name);
1138
- if (isLink(link2) && realpathOrNull2(link2) === null) unlinkSync(link2);
1170
+ if (isLink(link2) && realpathOrNull(link2) === null) unlinkSync(link2);
1139
1171
  else if (entryExists(link2)) {
1140
- if (realpathOrNull2(link2) !== realpathOrNull2(path)) skipped.push(link2);
1172
+ if (realpathOrNull(link2) !== realpathOrNull(path)) skipped.push(link2);
1141
1173
  continue;
1142
1174
  }
1143
- mkdirSync5(dir, { recursive: true });
1175
+ mkdirSync4(dir, { recursive: true });
1144
1176
  linkDir(path, link2);
1145
1177
  linked.push(link2);
1146
1178
  }
@@ -1150,8 +1182,8 @@ function createSkill(name, description) {
1150
1182
  if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) return { ok: false, reason: "use lowercase letters, digits and dashes" };
1151
1183
  const dir = librarySkillDir(name);
1152
1184
  if (existsSync7(dir)) return { ok: false, reason: `${name} already exists` };
1153
- mkdirSync5(dir, { recursive: true });
1154
- writeFileSync5(
1185
+ mkdirSync4(dir, { recursive: true });
1186
+ writeFileSync2(
1155
1187
  join8(dir, "SKILL.md"),
1156
1188
  `---
1157
1189
  name: ${name}
@@ -1175,27 +1207,29 @@ function listBackups() {
1175
1207
  const match = entry.match(/^(.*)-(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-\d{3}Z$/);
1176
1208
  if (!match) return [];
1177
1209
  const name = match[1];
1178
- const from = index[entry] ?? (kind === "trash" ? librarySkillDir(name) : join8(claudeDir(), "skills", name));
1179
- 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 }];
1180
1212
  });
1181
1213
  }).sort((a, b) => b.path.slice(-24).localeCompare(a.path.slice(-24)));
1182
1214
  }
1183
1215
  function backupFrom(backup) {
1184
1216
  if (backup.kind === "plugin") return "Claude Code plugin";
1185
- 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));
1186
1219
  }
1187
1220
  function restoreBackup(backup) {
1188
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)}` };
1189
1223
  if (backup.kind === "tidy-backup" && isLink(to)) unlinkSync(to);
1190
- 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 });
1191
1225
  let taken = existsSync7(to);
1192
1226
  try {
1193
1227
  taken = taken || lstatSync2(to) !== null;
1194
1228
  } catch {
1195
1229
  }
1196
1230
  if (taken) return { ok: false, reason: `${backup.name} already exists there` };
1197
- mkdirSync5(join8(to, ".."), { recursive: true });
1198
- renameSync(backup.path, to);
1231
+ mkdirSync4(join8(to, ".."), { recursive: true });
1232
+ move(backup.path, to);
1199
1233
  forgetLatest(backup.name);
1200
1234
  return { ok: true, to };
1201
1235
  }
@@ -1203,7 +1237,7 @@ function restoreBackup(backup) {
1203
1237
  // src/agentSkill.ts
1204
1238
  var AGENT_SKILL_NAME = "skilllib";
1205
1239
  function shippedSkill() {
1206
- 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");
1207
1241
  }
1208
1242
  function agentSkillDirs(enabled = enabledHarnesses()) {
1209
1243
  const claudeSkills = join9(claudeDir(), "skills");
@@ -1229,7 +1263,7 @@ function entryExists2(path) {
1229
1263
  function agentSkillState(enabled = enabledHarnesses()) {
1230
1264
  const [primary, ...links] = agentSkillDirs(enabled);
1231
1265
  if (!primary || !isOurs(primary) || links.some((l) => !existsSync8(join9(l, "SKILL.md")))) return "missing";
1232
- return readFileSync7(join9(primary, "SKILL.md"), "utf-8") === shippedSkill() ? "installed" : "outdated";
1266
+ return readFileSync4(join9(primary, "SKILL.md"), "utf-8") === shippedSkill() ? "installed" : "outdated";
1233
1267
  }
1234
1268
  function installAgentSkill(enabled = enabledHarnesses()) {
1235
1269
  const [primary, ...links] = agentSkillDirs(enabled);
@@ -1237,12 +1271,12 @@ function installAgentSkill(enabled = enabledHarnesses()) {
1237
1271
  const foreign = [primary, ...links].find((d) => entryExists2(d) && !isOurs(d) && !isDangling(d));
1238
1272
  if (foreign) return { ok: false, reason: `${tildify(foreign)} already exists and isn't skilllib's` };
1239
1273
  for (const d of [primary, ...links]) if (isDangling(d)) unlinkSync2(d);
1240
- mkdirSync6(primary, { recursive: true });
1241
- writeFileSync6(join9(primary, "SKILL.md"), shippedSkill());
1242
- 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");
1243
1277
  for (const link2 of links) {
1244
1278
  if (entryExists2(link2)) continue;
1245
- mkdirSync6(join9(link2, ".."), { recursive: true });
1279
+ mkdirSync5(join9(link2, ".."), { recursive: true });
1246
1280
  linkDir(primary, link2);
1247
1281
  }
1248
1282
  return { ok: true, dirs: [primary, ...links] };
@@ -1251,8 +1285,9 @@ function removeAgentSkill() {
1251
1285
  const all = agentSkillDirs(["claude-code", "codex"]);
1252
1286
  const removed = all.filter((d) => entryExists2(d) && (isOurs(d) || isDangling(d)));
1253
1287
  for (const d of removed) {
1288
+ if (!entryExists2(d)) continue;
1254
1289
  if (lstatSync3(d).isSymbolicLink()) unlinkSync2(d);
1255
- else rmSync3(d, { recursive: true, force: true });
1290
+ else rmSync4(d, { recursive: true, force: true });
1256
1291
  }
1257
1292
  return removed;
1258
1293
  }
@@ -1264,8 +1299,9 @@ function refreshAgentSkill() {
1264
1299
  }
1265
1300
 
1266
1301
  // src/commands.ts
1267
- import { existsSync as existsSync13, readFileSync as readFileSync9 } from "fs";
1268
- import { basename as basename9, join as join14, resolve as resolve7, sep as sep5 } from "path";
1302
+ import { existsSync as existsSync14, readFileSync as readFileSync6 } from "fs";
1303
+ import { parseArgs as parseFlags } from "util";
1304
+ import { basename as basename9, join as join15, resolve as resolve7, sep as sep6 } from "path";
1269
1305
 
1270
1306
  // src/git.ts
1271
1307
  import { execFileSync as execFileSync2 } from "child_process";
@@ -1307,24 +1343,17 @@ function relativeTo(root, path) {
1307
1343
  }
1308
1344
 
1309
1345
  // src/here.ts
1310
- import { readdirSync as readdirSync7 } from "fs";
1346
+ import { readdirSync as readdirSync8 } from "fs";
1311
1347
  import { basename as basename7 } from "path";
1312
1348
 
1313
1349
  // src/health.ts
1314
- import { lstatSync as lstatSync4 } from "fs";
1315
- 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";
1316
1352
 
1317
1353
  // src/plugins.ts
1318
1354
  import { execFileSync as execFileSync3 } from "child_process";
1319
- import { existsSync as existsSync10, mkdirSync as mkdirSync7, readdirSync as readdirSync5, readFileSync as readFileSync8, statSync as statSync3, writeFileSync as writeFileSync7 } from "fs";
1320
- import { basename as basename4, dirname as dirname3, join as join11, resolve as resolve6 } from "path";
1321
- function readJson2(path) {
1322
- try {
1323
- return JSON.parse(readFileSync8(path, "utf-8"));
1324
- } catch {
1325
- return null;
1326
- }
1327
- }
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";
1328
1357
  function nonEmptyDir(path) {
1329
1358
  try {
1330
1359
  return readdirSync5(path).some((f) => !f.startsWith("."));
@@ -1333,7 +1362,7 @@ function nonEmptyDir(path) {
1333
1362
  }
1334
1363
  }
1335
1364
  function extrasOf(root) {
1336
- const manifest = readJson2(join11(root, ".claude-plugin", "plugin.json")) ?? {};
1365
+ const manifest = readJson(join11(root, ".claude-plugin", "plugin.json")) ?? {};
1337
1366
  return [
1338
1367
  ...existsSync10(join11(root, ".mcp.json")) || manifest.mcpServers ? ["MCP servers"] : [],
1339
1368
  ...existsSync10(join11(root, "hooks", "hooks.json")) || manifest.hooks ? ["hooks"] : [],
@@ -1342,21 +1371,20 @@ function extrasOf(root) {
1342
1371
  ];
1343
1372
  }
1344
1373
  function claudePlugins(machine, root) {
1345
- const installed = readJson2(join11(claudeDir(), "plugins", "installed_plugins.json"))?.plugins ?? {};
1374
+ const installed = installedPluginsJson();
1346
1375
  const byOrigin = /* @__PURE__ */ new Map();
1347
1376
  for (const m of machine) if (m.kind === "plugin") byOrigin.set(m.origin, [...byOrigin.get(m.origin) ?? [], m.path]);
1348
1377
  return [...byOrigin].map(([origin, skills]) => {
1349
1378
  const synced = origin.endsWith("@synced");
1350
- const id = origin;
1351
1379
  const entries = installed[origin] ?? [];
1352
1380
  const entry = entries.find((e) => root && e.projectPath === root) ?? entries.find((e) => (e.scope ?? "user") === "user") ?? entries[0];
1353
1381
  return {
1354
- id,
1382
+ id: origin,
1355
1383
  scope: synced ? null : entry?.scope ?? "user",
1356
1384
  ...entry?.projectPath ? { projectPath: entry.projectPath } : {},
1357
1385
  synced,
1358
1386
  skills,
1359
- extras: extrasOf(dirname3(dirname3(skills[0])))
1387
+ extras: extrasOf(dirname4(dirname4(skills[0])))
1360
1388
  };
1361
1389
  });
1362
1390
  }
@@ -1369,10 +1397,10 @@ function newer(a, b) {
1369
1397
  }
1370
1398
  function updateOf(name, marketplace, entry, catalogs) {
1371
1399
  const dir = join11(claudeDir(), "plugins", "marketplaces", marketplace);
1372
- 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 ?? []);
1373
1401
  const listed = catalogs.get(marketplace).find((p) => p.name === name);
1374
1402
  if (!listed) return void 0;
1375
- 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);
1376
1404
  if (version && entry.version) return newer(version, entry.version) ? version : void 0;
1377
1405
  const sha = typeof listed.source === "object" ? listed.source?.sha : void 0;
1378
1406
  return sha && entry.gitCommitSha && sha !== entry.gitCommitSha ? "newer" : void 0;
@@ -1382,12 +1410,12 @@ function pluginRoot(id, entry) {
1382
1410
  return candidates.find((c) => existsSync10(join11(c, "skills"))) ?? candidates.find((c) => existsSync10(c)) ?? candidates[0];
1383
1411
  }
1384
1412
  function describe(root) {
1385
- const manifest = readJson2(join11(root, ".claude-plugin", "plugin.json")) ?? {};
1413
+ const manifest = readJson(join11(root, ".claude-plugin", "plugin.json")) ?? {};
1386
1414
  return { description: manifest.description ?? "", version: manifest.version, skills: skillDirsIn(join11(root, "skills")), extras: extrasOf(root) };
1387
1415
  }
1388
1416
  function installedPlugins() {
1389
1417
  const plugins = join11(claudeDir(), "plugins");
1390
- const installed = readJson2(join11(plugins, "installed_plugins.json"))?.plugins ?? {};
1418
+ const installed = installedPluginsJson();
1391
1419
  const catalogs = /* @__PURE__ */ new Map();
1392
1420
  const fromMarketplaces = Object.entries(installed).flatMap(
1393
1421
  ([id, entries]) => entries.map((entry) => {
@@ -1419,18 +1447,18 @@ function installedPlugins() {
1419
1447
  const syncedDir = join11(plugins, "synced");
1420
1448
  const dirs = (p) => existsSync10(p) ? readdirSync5(p, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".")).map((d) => join11(p, d.name)) : [];
1421
1449
  const fromSynced = dirs(syncedDir).flatMap(dirs).map((root) => {
1422
- 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`;
1423
1451
  return { id, scope: null, synced: true, agent: "claude-code", on: user[id] !== false, root, ...describe(root) };
1424
1452
  });
1425
1453
  return [...fromMarketplaces, ...fromSettings, ...fromSynced];
1426
1454
  }
1427
1455
  function cursorPlugins() {
1428
1456
  const cache = join11(userHome(), ".cursor", "plugins", "cache");
1429
- 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()) : [];
1430
1458
  return dirs(cache).flatMap(
1431
1459
  (marketplace) => dirs(join11(cache, marketplace)).flatMap((plugin) => {
1432
1460
  const versions = dirs(join11(cache, marketplace, plugin)).map((v) => join11(cache, marketplace, plugin, v));
1433
- 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];
1434
1462
  if (!root) return [];
1435
1463
  const about = describe(root);
1436
1464
  return [{ id: `${plugin}@${marketplace}`, scope: null, synced: false, agent: "cursor", on: null, root, ...about, version: about.version ?? basename4(root) }];
@@ -1469,11 +1497,10 @@ function claude(args, cwd, bin = claudeBinary()) {
1469
1497
  }
1470
1498
  }
1471
1499
  function setPluginIn(file, id, on) {
1472
- const settings = existsSync10(file) ? readJson2(file) : {};
1500
+ const settings = existsSync10(file) ? readJson(file) : {};
1473
1501
  if (!settings) return { ok: false, reason: `can't read ${file}` };
1474
1502
  const enabled = settings.enabledPlugins ?? {};
1475
- mkdirSync7(dirname3(file), { recursive: true });
1476
- writeFileSync7(file, JSON.stringify({ ...settings, enabledPlugins: { ...enabled, [id]: on } }, null, 2) + "\n");
1503
+ writeState(file, { ...settings, enabledPlugins: { ...enabled, [id]: on } });
1477
1504
  return { ok: true };
1478
1505
  }
1479
1506
  function ignoreLocalSettings(root) {
@@ -1496,9 +1523,9 @@ function ignoreLocalSettings(root) {
1496
1523
  }
1497
1524
  try {
1498
1525
  const exclude = resolve6(root, git2("rev-parse", "--git-path", "info/exclude"));
1499
- const before = existsSync10(exclude) ? readFileSync8(exclude, "utf-8") : "";
1500
- mkdirSync7(dirname3(exclude), { recursive: true });
1501
- 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}
1502
1529
  `);
1503
1530
  return ", which git now ignores via .git/info/exclude";
1504
1531
  } catch {
@@ -1566,32 +1593,38 @@ function removePlugin(plugin, how) {
1566
1593
  if (plugin.scope && plugin.scope !== "user" && !plugin.projectPath) {
1567
1594
  return { ok: false, message: `${plugin.id}: installed for one project; run \`claude plugin ${how === "delete" ? "uninstall" : "disable"} ${plugin.id} --scope ${plugin.scope}\` there` };
1568
1595
  }
1569
- if (!plugin.synced && !claudeBinary()) {
1596
+ const viaSettings = plugin.synced || !claudeBinary() && how === "off" && !plugin.projectPath;
1597
+ if (!viaSettings && !claudeBinary()) {
1570
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)` };
1571
1599
  }
1572
- 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);
1573
1603
  const scope = plugin.scope ? ["--scope", plugin.scope] : [];
1574
- const r = plugin.synced ? (
1604
+ const r = viaSettings ? (
1575
1605
  // Synced plugins aren't installed, so `claude plugin` can't touch them: their switch is enabledPlugins.
1576
1606
  setPluginIn(join11(claudeDir(), "settings.json"), plugin.id, false)
1577
- ) : 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
+ );
1578
1611
  const saved = imported.length ? `; ${imported.join(", ")} copied into Your skills` : "";
1579
1612
  if (!r.ok) return { ok: false, message: `${plugin.id}: ${r.reason}${saved}` };
1580
- 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);
1581
1614
  return { ok: true, message: `${plugin.id} ${how === "delete" && !plugin.synced ? "removed" : "turned off"}${saved}` };
1582
1615
  }
1583
1616
  function recordRemovedPlugin(id, scope, projectPath, on = true) {
1584
- const list2 = readJson2(removedFile()) ?? [];
1585
- mkdirSync7(skilllibHome(), { recursive: true });
1586
- 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() }]);
1587
1620
  }
1588
1621
  function pluginBackups() {
1589
- return (readJson2(removedFile()) ?? []).map((r) => ({
1622
+ return (readState(removedFile()) ?? []).map((r) => ({
1590
1623
  name: r.id,
1591
1624
  kind: "plugin",
1592
1625
  // Project and local installs go back into their project.
1593
1626
  path: r.projectPath ? `${r.scope}:${r.projectPath}` : r.scope,
1594
- movedAt: r.at.slice(0, 16).replace("T", " "),
1627
+ movedAt: localStamp(Date.parse(r.at)),
1595
1628
  from: r.id
1596
1629
  }));
1597
1630
  }
@@ -1600,11 +1633,11 @@ function restorePlugin(backup) {
1600
1633
  const projectPath = rest.join(":") || void 0;
1601
1634
  const r = claude(["plugin", "install", backup.from, "--scope", scope], projectPath);
1602
1635
  if (!r.ok) return r;
1603
- const records = readJson2(removedFile()) ?? [];
1636
+ const records = readState(removedFile()) ?? [];
1604
1637
  const same = (x) => x.id === backup.from && (x.projectPath ?? void 0) === projectPath && x.scope === scope;
1605
1638
  const wasOff = records.filter(same).pop()?.on === false;
1606
1639
  const off = wasOff ? claude(["plugin", "disable", backup.from, "--scope", scope], projectPath) : null;
1607
- writeFileSync7(removedFile(), JSON.stringify(records.filter((x) => !same(x)), null, 2) + "\n");
1640
+ writeState(removedFile(), records.filter((x) => !same(x)));
1608
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" : ""})`;
1609
1642
  return { ok: true, to: where };
1610
1643
  }
@@ -1613,14 +1646,14 @@ function cursorPluginSkills(plugins = cursorPlugins()) {
1613
1646
  }
1614
1647
 
1615
1648
  // src/tidy.ts
1616
- import { existsSync as existsSync11, mkdirSync as mkdirSync8, readdirSync as readdirSync6, unlinkSync as unlinkSync3 } from "fs";
1617
- 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";
1618
1651
  function entriesOf(dirs, name) {
1619
1652
  return dirs.flatMap((dir) => {
1620
1653
  const path = join12(dir, name);
1621
1654
  if (!entryExists(path)) return [];
1622
1655
  const link2 = isLink(path);
1623
- const real = realpathOrNull2(path);
1656
+ const real = realpathOrNull(path);
1624
1657
  const skill = existsSync11(join12(path, "SKILL.md"));
1625
1658
  if (!link2 && !skill) return [];
1626
1659
  return [{ dir, path, link: link2, real, hash: null }];
@@ -1645,7 +1678,7 @@ function globalTidyDirs() {
1645
1678
  function runsFrom(id, dirsWithCopy, readsOf) {
1646
1679
  const reads = readsOf(id).filter((d) => dirsWithCopy.includes(d));
1647
1680
  if (id === "codex") return reads;
1648
- const pick = (d) => CURSOR_PICKS.indexOf(basename5(dirname4(d)));
1681
+ const pick = (d) => CURSOR_PICKS.indexOf(basename5(dirname5(d)));
1649
1682
  return id === "cursor" ? [...reads].sort((a, b) => pick(a) - pick(b)).slice(0, 1) : reads.slice(0, 1);
1650
1683
  }
1651
1684
  function conflictOf(name, root, reals, enabled, readsOf) {
@@ -1661,17 +1694,17 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
1661
1694
  const { skills } = readManifest(root);
1662
1695
  const abs = (dir) => join12(root, dir);
1663
1696
  const rel = (dir) => relativeTo(root, dir);
1664
- const readsOf = (id) => HARNESSES.find((h) => h.id === id).projectDirs.map(abs);
1697
+ const readsOf = (id) => harness(id).projectDirs.map(abs);
1665
1698
  let git2;
1666
1699
  const skillsLock = projectSkillsLock(root);
1667
1700
  for (const name of namesIn(PROJECT_TIDY_DIRS.map(abs))) {
1668
1701
  const entries = entriesOf(PROJECT_TIDY_DIRS.map(abs), name);
1669
- 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));
1670
1703
  if (external) {
1671
1704
  report.skipped.push({ name, reason: `${rel(external.path)} links outside the repo` });
1672
1705
  continue;
1673
1706
  }
1674
- 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);
1675
1708
  if (reals.length === 0) continue;
1676
1709
  hashReals(reals);
1677
1710
  const dep = skills[name];
@@ -1697,10 +1730,11 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
1697
1730
  const wanted = new Set([...installDirs(enabled, rel(primary.dir)), ...dep?.links ?? []].map(abs));
1698
1731
  const steps = [];
1699
1732
  for (const e of entries) {
1700
- if (e === primary) continue;
1733
+ if (e === primary || !e.link && e.real === primary.real) continue;
1701
1734
  const needed = wanted.has(e.dir);
1702
1735
  if (e.link) {
1703
1736
  if (e.real === primary.real) {
1737
+ if (!needed && !dep && ["committed", "changed"].includes((git2 ??= gitInfo(root))?.of(rel(e.path)) ?? "")) continue;
1704
1738
  if (!needed) steps.push({ kind: "unlink", path: e.path });
1705
1739
  } else steps.push(needed ? { kind: "link", path: e.path, target: primary.path } : { kind: "unlink", path: e.path });
1706
1740
  } else {
@@ -1713,7 +1747,7 @@ function planProjectTidy(root, { keep = {}, enabled = enabledHarnesses() } = {})
1713
1747
  return report;
1714
1748
  }
1715
1749
  function globalReadsOf(id) {
1716
- return HARNESSES.find((h) => h.id === id).globalDirs().map(globalDirAs);
1750
+ return harness(id).globalDirs().map(globalDirAs);
1717
1751
  }
1718
1752
  function isSkillsDirPlugin(path) {
1719
1753
  return existsSync11(join12(path, ".claude-plugin", "plugin.json"));
@@ -1728,7 +1762,7 @@ function planGlobalTidy({ keep = {}, enabled = enabledHarnesses() } = {}) {
1728
1762
  report.skipped.push({ name, reason: "a Claude Code plugin kept in a skills folder" });
1729
1763
  continue;
1730
1764
  }
1731
- 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);
1732
1766
  if (reals.length < 2) continue;
1733
1767
  hashReals(reals);
1734
1768
  const kept = reals.find((r) => r.dir === keep[name]);
@@ -1754,11 +1788,12 @@ function applyTidy(plan, { git: git2 = "keep", info: info2 } = {}) {
1754
1788
  const applied = [];
1755
1789
  for (const step of plan.steps) {
1756
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;
1757
1792
  if (step.kind === "link" || step.kind === "unlink") {
1758
1793
  if (isLink(step.path)) unlinkSync3(step.path);
1759
1794
  } else stash(step.path, "tidy-backup");
1760
1795
  if (step.kind === "link" || step.kind === "replace") {
1761
- mkdirSync8(dirname4(step.path), { recursive: true });
1796
+ mkdirSync7(dirname5(step.path), { recursive: true });
1762
1797
  linkDir(step.target, step.path);
1763
1798
  }
1764
1799
  applied.push(step);
@@ -1766,7 +1801,7 @@ function applyTidy(plan, { git: git2 = "keep", info: info2 } = {}) {
1766
1801
  if (plan.root) {
1767
1802
  const manifest = readManifest(plan.root);
1768
1803
  const dep = manifest.skills[plan.name];
1769
- 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)));
1770
1805
  if (dep && added.length) {
1771
1806
  manifest.skills[plan.name] = { ...dep, links: [.../* @__PURE__ */ new Set([...dep.links ?? [], ...added])] };
1772
1807
  writeManifest(plan.root, manifest);
@@ -1810,42 +1845,135 @@ function orThrow(r) {
1810
1845
  if (!r.ok) throw new Error(r.message);
1811
1846
  return r.message;
1812
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
+ }
1813
1866
  var plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
1814
- 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(", ");
1815
1869
  function conflictIssue(c) {
1816
1870
  const where = c.root ? basename6(c.root) : "your global folders";
1817
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
+ };
1818
1879
  return {
1819
1880
  id: `conflict:${c.root ?? "global"}:${c.name}`,
1820
1881
  severity: "problem",
1821
1882
  title: `${c.name} in ${where}: the copies differ`,
1822
- 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.`,
1823
- choices: winners.map((x) => ({
1824
- label: `Keep the ${copyLabel(c, x.dir)} copy`,
1825
- hint: x.dir === c.managed ? "skilllib's version" : "the others become links to it",
1826
- run: () => {
1827
- const keep = { [c.name]: x.dir };
1828
- const plan = (c.root ? planProjectTidy(c.root, { keep }) : planGlobalTidy({ keep })).plans.find((p) => p.name === c.name);
1829
- if (plan) applyTidy(plan, { git: "go" });
1830
- return `${c.name}: kept the ${copyLabel(c, x.dir)} copy`;
1831
- }
1832
- }))
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
+ })
1833
1895
  };
1834
1896
  }
1835
1897
  function applyAll(plans, git2) {
1836
1898
  const results = plans.map((p) => applyTidy(p, { git: git2 }));
1837
1899
  return { skills: results.filter((r) => r.applied.length).length, held: results.reduce((n, r) => n + r.held.length, 0) };
1838
1900
  }
1839
- function findIssues(projects2, machine, libraryNames, statusOf = projectStatus, loadedIn = (root) => skillsLoadedIn(root, machine), nestedOf = nestedSkills) {
1840
- 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
+ }));
1841
1967
  const statuses = new Map(projects2.map((root) => [root, statusOf(root)]));
1968
+ issues.push(...grokDiffers(projects2, machine, (root) => statuses.get(root)));
1842
1969
  const nested = new Map(projects2.map((root) => [root, nestedOf(root)]));
1843
1970
  for (const skill of machine.filter((m) => m.broken)) {
1844
1971
  issues.push({
1845
- id: `broken:${skill.name}`,
1972
+ id: `broken:${skill.path}`,
1846
1973
  severity: "problem",
1847
- title: `${skill.name}: broken link in ${tildify(dirname5(skill.path))}`,
1974
+ title: `${skill.name}: broken link in ${tildify(dirname6(skill.path))}`,
1848
1975
  detail: "It points at a folder that no longer exists, so it loads nothing.",
1976
+ at: rows(null, [skill.name]),
1849
1977
  fix: {
1850
1978
  label: "Remove the link",
1851
1979
  run: () => {
@@ -1868,7 +1996,8 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
1868
1996
  id: `dup:${name}`,
1869
1997
  severity: "problem",
1870
1998
  title: `${name}: loaded twice in ${harnesses.join(" and ")}`,
1871
- 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])
1872
2001
  });
1873
2002
  }
1874
2003
  const loadedHere = new Map(projects2.map((root) => [root, loadedIn(root)]));
@@ -1913,7 +2042,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
1913
2042
  const pluginName = plugin.id.split("@")[0];
1914
2043
  const remove2 = {
1915
2044
  label: `Remove the plugin ${plugin.id}`,
1916
- 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",
1917
2046
  run: () => orThrow(removePlugin(plugin, "delete"))
1918
2047
  };
1919
2048
  const off = { label: `Turn the plugin ${plugin.id} off`, hint: "stays installed", run: () => orThrow(removePlugin(plugin, "off")) };
@@ -1934,9 +2063,25 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
1934
2063
  plugin.synced ? "It's synced from claude.ai, so it can only be turned off here." : "",
1935
2064
  inRepos.length ? `Or keep it and turn it off only in ${inRepos.map(([root]) => basename6(root)).join(", ")}.` : ""
1936
2065
  ].filter(Boolean).join(" "),
1937
- 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
1938
2070
  });
1939
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
+ }
1940
2085
  const machinePluginSkills = new Set(machinePlugins.flatMap((p) => p.skills));
1941
2086
  for (const root of projects2) {
1942
2087
  const where = basename6(root);
@@ -1951,7 +2096,9 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
1951
2096
  severity: "problem",
1952
2097
  title: `${dupes.join(", ")}: also in the Claude Code plugin ${plugin.id}, on in ${where}`,
1953
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.`,
1954
- choices: repoChoices(plugin, root, inRepo)
2099
+ choices: repoChoices(plugin, root, inRepo),
2100
+ at: rows(root, dupes),
2101
+ plugin: plugin.id
1955
2102
  });
1956
2103
  }
1957
2104
  }
@@ -1963,7 +2110,9 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
1963
2110
  id: `cursor-plugin:${plugin}`,
1964
2111
  severity: "suggestion",
1965
2112
  title: `${names.join(", ")}: also in the Cursor plugin ${plugin}`,
1966
- 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
1967
2116
  });
1968
2117
  }
1969
2118
  }
@@ -1973,7 +2122,8 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
1973
2122
  id: "tidy:global",
1974
2123
  severity: "suggestion",
1975
2124
  title: `${plural(global.plans.length, "global skill")} with duplicate copies`,
1976
- 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)),
1977
2127
  fix: {
1978
2128
  label: "Keep one copy of each",
1979
2129
  run: () => {
@@ -1986,13 +2136,14 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
1986
2136
  }
1987
2137
  issues.push(...global.conflicts.map(conflictIssue));
1988
2138
  const kept = keptGlobal();
1989
- 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))];
1990
2140
  if (unreviewed.length) {
1991
2141
  issues.push({
1992
2142
  id: "review:global",
1993
2143
  severity: "suggestion",
1994
2144
  title: `${plural(unreviewed.length, "global skill")} of yours ${unreviewed.length === 1 ? "loads" : "load"} in every repo`,
1995
- 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)
1996
2147
  });
1997
2148
  }
1998
2149
  const enabled = enabledHarnesses();
@@ -2019,6 +2170,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
2019
2170
  severity: "suggestion",
2020
2171
  title: `${plural(skills.length, "global skill")}${onPurpose ? " you keep global" : ""} not usable by ${agentNames(missing)}`,
2021
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))]),
2022
2174
  ...onPurpose ? { fix: link2 } : { choices: [link2] }
2023
2175
  });
2024
2176
  }
@@ -2028,22 +2180,30 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
2028
2180
  for (const root of projects2) {
2029
2181
  const where = basename6(root);
2030
2182
  const status2 = statuses.get(root);
2031
- 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
+ });
2032
2197
  if (restorable.length > 0) {
2033
2198
  issues.push({
2034
2199
  id: `sync:${root}`,
2035
2200
  severity: "problem",
2036
2201
  title: `${where}: ${plural(restorable.length, "skill")} in skilllib.json not installed`,
2037
2202
  detail: `${restorable.map((s) => s.name).join(", ")}: the folder is missing`,
2203
+ at: rows(root, restorable.map((s) => s.name)),
2038
2204
  fix: {
2039
2205
  label: "Sync the project",
2040
- run: () => {
2041
- const changes = syncProject(root);
2042
- const done = changes.filter((c) => c.action !== "skipped");
2043
- const skipped = changes.filter((c) => c.action === "skipped").map((c) => `${c.name}: ${c.reason}`);
2044
- if (!done.length) throw new Error(`${where}: nothing restored${skipped.length ? `; ${skipped.join("; ")}` : ""}`);
2045
- return `${where}: restored ${plural(done.length, "skill")}${skipped.length ? `; skipped ${skipped.join("; ")}` : ""}`;
2046
- }
2206
+ run: () => summarize(where, "restored", syncProject(root))
2047
2207
  }
2048
2208
  });
2049
2209
  }
@@ -2053,6 +2213,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
2053
2213
  severity: "problem",
2054
2214
  title: `${s.name}: pinned in ${where}'s skilllib.json, but not in your library`,
2055
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]),
2056
2217
  // Editing skilllib.json is the team's call: a choice, never run by `doctor --fix`.
2057
2218
  choices: [
2058
2219
  {
@@ -2073,15 +2234,10 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
2073
2234
  severity: "suggestion",
2074
2235
  title: `${where}: ${plural(behind.length, "skill")} out of date`,
2075
2236
  detail: behind.map((s) => `${s.name} (v${s.version} \u2192 v${s.latest})`).join(", "),
2237
+ at: rows(root, behind.map((s) => s.name)),
2076
2238
  fix: {
2077
2239
  label: "Update to the newest versions",
2078
- run: () => {
2079
- const changes = updateProject(root, behind.map((s) => s.name));
2080
- const done = changes.filter((c) => c.action !== "skipped");
2081
- const skipped = changes.filter((c) => c.action === "skipped").map((c) => `${c.name}: ${c.reason}`);
2082
- if (!done.length) throw new Error(`${where}: nothing updated${skipped.length ? `; ${skipped.join("; ")}` : ""}`);
2083
- return `${where}: updated ${plural(done.length, "skill")}${skipped.length ? `; skipped ${skipped.join("; ")}` : ""}`;
2084
- }
2240
+ run: () => summarize(where, "updated", updateProject(root, behind.map((s) => s.name)))
2085
2241
  }
2086
2242
  });
2087
2243
  const tidy2 = planProjectTidy(root);
@@ -2097,40 +2253,45 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
2097
2253
  id: `tidy:${root}`,
2098
2254
  severity: "suggestion",
2099
2255
  title: `${where}: ${plural(tidy2.plans.length, "skill")} with extra copies or links`,
2100
- 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)),
2101
2258
  ...visible2.length ? {
2102
2259
  choices: [
2103
- { 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") }] : [],
2104
2262
  { label: "Tidy everything", hint: `${plural(visible2.length, "change")} will show in git status`, run: run("go") }
2105
2263
  ]
2106
2264
  } : { fix: { label: "Tidy", run: run("go") } }
2107
2265
  });
2108
2266
  }
2109
2267
  issues.push(...tidy2.conflicts.map(conflictIssue));
2110
- 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 }) }));
2111
2270
  for (const { s, would } of blindHere.filter(({ would: w }) => !w.created.length && !w.blocked.length && w.uncommitted.length))
2112
2271
  issues.push({
2113
2272
  id: `uncommitted:${root}:${s.name}`,
2114
2273
  severity: "suggestion",
2115
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`,
2116
- 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])
2117
2277
  });
2118
2278
  const partial = blindHere.filter(({ would }) => would.created.length || would.blocked.length).map(({ s }) => s);
2119
2279
  if (partial.length) {
2120
- 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)))];
2121
2281
  const tracked = [...new Set(blindHere.flatMap(({ would }) => would.blocked))];
2122
2282
  const run = (allowTracked) => () => {
2123
2283
  if (allowTracked) allowTrackedLinks(root);
2124
2284
  const r = linkAll(root, { allowTracked });
2125
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)` : ""}`;
2126
2286
  if (!r.linked.length) throw new Error(`${where}: nothing linked${notes}`);
2127
- 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}`;
2128
2288
  };
2129
2289
  issues.push({
2130
2290
  id: `usable:${root}`,
2131
2291
  severity: "suggestion",
2132
- title: `${where}: ${plural(partial.length, "skill")} not usable by ${agentNames(missing)}`,
2292
+ title: `${where}: ${plural(partial.length, "skill")} not usable by ${agentNames(missing2)}`,
2133
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)),
2134
2295
  ...tracked.length ? {
2135
2296
  choices: [
2136
2297
  // Only when some link goes where git doesn't track: otherwise this one would link nothing.
@@ -2151,12 +2312,13 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
2151
2312
  severity: "suggestion",
2152
2313
  title: `${s.name} in ${where} isn't in your library`,
2153
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]),
2154
2316
  choices: [
2155
2317
  {
2156
2318
  label: "Copy into the library",
2157
2319
  hint: `${where}'s copy stays as it is`,
2158
2320
  run: () => {
2159
- importSkill(s.path);
2321
+ intoLibrary(s.path);
2160
2322
  return `${s.name} copied into your library; ${where}'s copy stays as it is`;
2161
2323
  }
2162
2324
  }
@@ -2168,13 +2330,14 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
2168
2330
  severity: "suggestion",
2169
2331
  title: `${s.name} in ${where} isn't in your library`,
2170
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]),
2171
2334
  choices: [
2172
2335
  {
2173
2336
  label: "Import into the library",
2174
- hint: `${where} then tracks it`,
2337
+ hint: "copies it into Your skills",
2175
2338
  run: () => {
2176
- importSkill(s.path);
2177
- addSkill(root, s.name);
2339
+ intoLibrary(s.path);
2340
+ track(root, s.name);
2178
2341
  return `${s.name} imported and tracked in ${where}`;
2179
2342
  }
2180
2343
  }
@@ -2186,9 +2349,81 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
2186
2349
  severity: "suggestion",
2187
2350
  title: `${s.name} in ${where} isn't tracked`,
2188
2351
  detail: "It's identical to the library copy; track it (in skilllib.json) so future library changes reach it.",
2189
- 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
+ ]
2190
2407
  });
2191
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
+ });
2192
2427
  if (s.state === "edited locally" || s.state === "edited locally, update available") {
2193
2428
  const newer2 = s.state !== "edited locally";
2194
2429
  const pinned = s.version ? `v${s.version}` : "the library version";
@@ -2197,6 +2432,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
2197
2432
  severity: "suggestion",
2198
2433
  title: `${s.name} in ${where} has local edits`,
2199
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]),
2200
2436
  choices: [
2201
2437
  {
2202
2438
  label: "Save as a new version in Your skills",
@@ -2210,7 +2446,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
2210
2446
  },
2211
2447
  {
2212
2448
  label: "Discard the edits",
2213
- hint: `back to ${pinned}; the edited copy is restorable from Health`,
2449
+ hint: `back to ${pinned}; the edited copy is restorable with skilllib restore`,
2214
2450
  run: () => {
2215
2451
  const c = discardEdits(root, s.name);
2216
2452
  if (c.action === "skipped") throw new Error(`${s.name}: ${c.reason}`);
@@ -2226,13 +2462,14 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
2226
2462
  for (const root of projects2) {
2227
2463
  for (const n of nested.get(root)) {
2228
2464
  const copies = alsoGlobal.get(n.name) ?? [];
2229
- 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 }]);
2230
2466
  }
2231
2467
  }
2232
2468
  for (const [name, copies] of alsoGlobal) {
2233
2469
  const wheres = copies.map((c) => basename6(c.root)).join(", ");
2234
2470
  const removable = copies.filter((c) => c.managed).map((c) => c.root);
2235
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])];
2236
2473
  if (kept.has(name)) {
2237
2474
  const removeWheres = removable.map((r) => basename6(r)).join(", ");
2238
2475
  issues.push({
@@ -2240,13 +2477,15 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
2240
2477
  severity: "suggestion",
2241
2478
  title: `${name}: in ${wheres}, and you keep it global`,
2242
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,
2243
2481
  choices: removable.length ? [
2244
2482
  {
2245
2483
  label: `Remove the copies in ${removeWheres}`,
2246
2484
  hint: "the global copy stays",
2247
2485
  run: () => {
2248
2486
  const removed = removable.filter((root) => removeSkill(root, name).action === "removed").map((r) => basename6(r));
2249
- 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`;
2250
2489
  }
2251
2490
  }
2252
2491
  ] : void 0
@@ -2255,18 +2494,23 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
2255
2494
  }
2256
2495
  const movable = machine.find((m) => m.name === name && m.movable);
2257
2496
  issues.push({
2497
+ at,
2258
2498
  id: `twice:${name}`,
2259
2499
  severity: "suggestion",
2260
2500
  title: `${name}: in ${wheres} and also loaded globally`,
2261
- 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.`}`,
2262
2502
  choices: movable ? [
2263
2503
  {
2264
2504
  label: "Stop loading it globally",
2265
- hint: "restorable from Health",
2505
+ hint: "restorable with skilllib restore",
2266
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)`);
2267
2509
  if (!libraryNames.has(name)) importSkill(movable.path);
2268
- const r = unloadGlobal(movable.path, movable.links);
2269
- 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
+ }
2270
2514
  return `${name} no longer loads globally`;
2271
2515
  }
2272
2516
  }
@@ -2289,6 +2533,7 @@ function findIssues(projects2, machine, libraryNames, statusOf = projectStatus,
2289
2533
  severity: "suggestion",
2290
2534
  title: agentSkill2 === "missing" ? "Your agents don't know about skilllib" : "The skilllib skill for your agents is out of date",
2291
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"]),
2292
2537
  ...agentSkill2 === "missing" ? { choices: [install] } : { fix: install }
2293
2538
  });
2294
2539
  }
@@ -2316,6 +2561,7 @@ function usageIssues(projects2, machine, libraryNames, uses, statusOf = projectS
2316
2561
  severity: "suggestion",
2317
2562
  title: `${where}: ${plural(unused.length, "skill")} unused in ${uses.days} days`,
2318
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)),
2319
2565
  choices: removable.length ? [
2320
2566
  {
2321
2567
  label: `Remove ${removable.length === unused.length ? "them" : `the ${plural(removable.length, "skill")} skilllib installed`} from ${where}`,
@@ -2324,12 +2570,30 @@ function usageIssues(projects2, machine, libraryNames, uses, statusOf = projectS
2324
2570
  const changes = removable.map((s) => removeSkill(root, s.name));
2325
2571
  const removed = changes.filter((c) => c.action === "removed").map((c) => c.name);
2326
2572
  const kept = changes.filter((c) => c.action !== "removed").map((c) => c.name);
2327
- 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)` : ""}`;
2328
2575
  }
2329
2576
  }
2330
2577
  ] : void 0
2331
2578
  });
2332
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
+ }
2333
2597
  const installed = /* @__PURE__ */ new Set([...projects2.flatMap((root) => statusOf(root).map((s) => s.name)), ...machine.filter((m) => !m.broken).map((m) => m.name)]);
2334
2598
  const idle = [...libraryNames].filter((name) => {
2335
2599
  const first = versionHistory(name)[0];
@@ -2341,13 +2605,16 @@ function usageIssues(projects2, machine, libraryNames, uses, statusOf = projectS
2341
2605
  severity: "suggestion",
2342
2606
  title: `${plural(idle.length, "skill")} in Your skills: in no repo, unused in ${uses.days} days`,
2343
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),
2344
2610
  choices: [
2345
2611
  {
2346
2612
  label: `Delete ${idle.length === 1 ? "it" : `all ${idle.length}`} from Your skills`,
2347
- hint: "restorable from Health",
2613
+ hint: "restorable with skilllib restore",
2348
2614
  run: () => {
2349
2615
  const done = idle.map((name) => [name, deleteLibrarySkill(name)]);
2350
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(", ")}`);
2351
2618
  return `Deleted ${plural(done.length - failed.length, "skill")} from Your skills${failed.length ? `; kept ${failed.join(", ")}` : ""}`;
2352
2619
  }
2353
2620
  }
@@ -2375,7 +2642,7 @@ function globalFrom(kind, origin, path) {
2375
2642
  var SKIP = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "target", "vendor", "coverage"]);
2376
2643
  function topLevel(root) {
2377
2644
  try {
2378
- 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);
2379
2646
  } catch {
2380
2647
  return [];
2381
2648
  }
@@ -2391,7 +2658,7 @@ function usableHere(root, uses = /* @__PURE__ */ new Map()) {
2391
2658
  const group = (list2) => groupBy(
2392
2659
  list2.map((s) => {
2393
2660
  const loadedBy = s.visibility.filter((v) => v.paths > 0).map((v) => v.id);
2394
- const behind = s.managed && s.latest !== null && s.version !== null && s.latest > s.version;
2661
+ const behind = s.state.endsWith("update available");
2395
2662
  const gitState = git2?.of(relativeTo(root, s.path));
2396
2663
  const repo = !s.managed && (s.state.startsWith("repo skill") || gitState === "committed" || gitState === "changed");
2397
2664
  const usual = s.state === "ok" || s.state === "local only" || repo && s.state !== "from npx skills";
@@ -2429,7 +2696,8 @@ function usableHere(root, uses = /* @__PURE__ */ new Map()) {
2429
2696
  const usesHere = Object.fromEntries([...uses].filter(([, n]) => n > 0));
2430
2697
  return {
2431
2698
  project: tildify(root),
2432
- 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) : [],
2433
2701
  agents,
2434
2702
  skills,
2435
2703
  nested: nested.length > 0 ? nested : void 0,
@@ -2444,7 +2712,7 @@ function firstSentence(description) {
2444
2712
  return sentence.length > 200 ? sentence.slice(0, 199).trimEnd() + "\u2026" : sentence;
2445
2713
  }
2446
2714
  function libraryFor(currentProject) {
2447
- const manifests = visibleProjects().map((p) => ({ name: basename7(p), skills: readManifest(p).skills }));
2715
+ const manifests = visibleProjects().map((p) => ({ name: basename7(p), skills: pinsIn(p) }));
2448
2716
  const here = currentProject ? new Set(projectStatus(currentProject).map((s) => s.name)) : null;
2449
2717
  const all = librarySkills();
2450
2718
  const describe2 = (s) => {
@@ -2456,8 +2724,8 @@ function libraryFor(currentProject) {
2456
2724
  }
2457
2725
 
2458
2726
  // src/usage.ts
2459
- import { createReadStream, existsSync as existsSync12, readdirSync as readdirSync8, statSync as statSync4 } from "fs";
2460
- 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";
2461
2729
  import { createInterface } from "readline";
2462
2730
  var BUILTIN_COMMANDS = /* @__PURE__ */ new Set([
2463
2731
  "add-dir",
@@ -2537,12 +2805,12 @@ function usesFromTranscriptLine(line) {
2537
2805
  return [];
2538
2806
  }
2539
2807
  function transcriptFiles(sinceMs) {
2540
- const base = join13(claudeDir(), "projects");
2541
- if (!existsSync12(base)) return [];
2542
- return readdirSync8(base).flatMap((project) => {
2543
- const dir = join13(base, project);
2544
- if (!statSync4(dir).isDirectory()) return [];
2545
- 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);
2546
2814
  });
2547
2815
  }
2548
2816
  async function scanUsage(days) {
@@ -2558,7 +2826,7 @@ async function scanUsage(days) {
2558
2826
  }
2559
2827
  return [...seen.values()];
2560
2828
  }
2561
- function summarize(uses, projectOf = () => null) {
2829
+ function summarize2(uses, projectOf = () => null) {
2562
2830
  const bySkill = /* @__PURE__ */ new Map();
2563
2831
  for (const use of uses) {
2564
2832
  const skill = use.skill.split(":").pop();
@@ -2585,7 +2853,12 @@ function usesByProject(uses, projectOf) {
2585
2853
 
2586
2854
  // src/move.ts
2587
2855
  import { basename as basename8 } from "path";
2588
- 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
+ } = {}) {
2589
2862
  const byName = /* @__PURE__ */ new Map();
2590
2863
  for (const m of skills) byName.set(m.name, [...byName.get(m.name) ?? [], m]);
2591
2864
  return [...byName].map(([name, copies]) => {
@@ -2596,7 +2869,14 @@ function moveGlobalSkills(skills, roots, { group, allowTracked = false, label =
2596
2869
  if (group) recordOrigin(name, `group: ${group}`);
2597
2870
  const changes = roots.map((root) => [root, addSkill(root, name, { allowTracked })]);
2598
2871
  const skipped = changes.filter(([, c]) => c.action === "skipped");
2599
- 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
+ }
2600
2880
  const failed = copies.map((m) => unloadGlobal(m.path, m.links)).find((r) => !r.ok);
2601
2881
  return failed && !failed.ok ? { name, moved: false, reason: failed.reason, changes } : { name, moved: true, changes };
2602
2882
  });
@@ -2604,23 +2884,36 @@ function moveGlobalSkills(skills, roots, { group, allowTracked = false, label =
2604
2884
 
2605
2885
  // src/commands.ts
2606
2886
  var USAGE_DAYS = 30;
2887
+ var FLAGS = {
2888
+ force: { type: "boolean" },
2889
+ all: { type: "boolean" },
2890
+ global: { type: "boolean" },
2891
+ fix: { type: "boolean" },
2892
+ "allow-tracked": { type: "boolean" },
2893
+ json: { type: "boolean" },
2894
+ "dry-run": { type: "boolean" },
2895
+ "allow-git": { type: "boolean" },
2896
+ days: { type: "string" },
2897
+ keep: { type: "string" },
2898
+ to: { type: "string", multiple: true }
2899
+ };
2607
2900
  function parseArgs(argv) {
2608
- const daysIdx = argv.indexOf("--days");
2609
- const days = daysIdx >= 0 ? Number(argv[daysIdx + 1]) : USAGE_DAYS;
2610
- const keepIdx = argv.indexOf("--keep");
2901
+ const { values: v, positionals } = parseFlags({ args: argv, options: FLAGS, allowPositionals: true, strict: true });
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})`);
2611
2904
  return {
2612
- positional: argv.filter((a, i) => !a.startsWith("--") && !["--days", "--keep", "--to"].includes(argv[i - 1])),
2613
- force: argv.includes("--force"),
2614
- all: argv.includes("--all"),
2615
- global: argv.includes("--global"),
2616
- fix: argv.includes("--fix"),
2617
- allowTracked: argv.includes("--allow-tracked"),
2618
- json: argv.includes("--json"),
2619
- days: Number.isFinite(days) && days > 0 ? days : USAGE_DAYS,
2620
- dryRun: argv.includes("--dry-run"),
2621
- allowGit: argv.includes("--allow-git"),
2622
- ...keepIdx >= 0 && argv[keepIdx + 1] ? { keep: argv[keepIdx + 1] } : {},
2623
- to: argv.flatMap((a, i) => argv[i - 1] === "--to" && !a.startsWith("--") ? a.split(",").filter(Boolean) : [])
2905
+ positional: positionals,
2906
+ force: !!v.force,
2907
+ all: !!v.all,
2908
+ global: !!v.global,
2909
+ fix: !!v.fix,
2910
+ allowTracked: !!v["allow-tracked"],
2911
+ json: !!v.json,
2912
+ days,
2913
+ dryRun: !!v["dry-run"],
2914
+ allowGit: !!v["allow-git"],
2915
+ ...v.keep ? { keep: v.keep } : {},
2916
+ to: (v.to ?? []).flatMap((t) => t.split(",").filter(Boolean))
2624
2917
  };
2625
2918
  }
2626
2919
  function colorState(state) {
@@ -2632,25 +2925,24 @@ function colorState(state) {
2632
2925
  function withDirs(root, changes) {
2633
2926
  const { skills } = readManifest(root);
2634
2927
  return changes.map((c) => {
2635
- 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;
2636
2929
  return dep ? { ...c, dirs: [dep.dir ?? ".claude/skills", ...dep.links ?? []] } : c;
2637
2930
  });
2638
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
+ }
2639
2938
  function printChanges(root, changes, args) {
2640
- if (changes.some((c) => c.action === "skipped")) process.exitCode = 1;
2939
+ if (changes.some(incomplete)) process.exitCode = 1;
2641
2940
  if (args.json) return json("changes", withDirs(root, changes));
2642
2941
  for (const c of changes) {
2643
- if (c.blocked?.length) {
2644
- warn(`${c.name}: git tracks ${c.blocked.join(", ")} here, so no link was added there. Re-run with --allow-tracked to add it.`);
2645
- }
2646
- if (c.uncommitted?.length) {
2647
- warn(
2648
- `${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.`
2649
- );
2650
- }
2942
+ warnHeldLinks(c);
2651
2943
  if (c.action === "skipped") error(`${c.name}: ${c.reason}`);
2652
2944
  else {
2653
- 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");
2654
2946
  const versions = c.action === "removed" ? "" : c.from && c.from !== c.to ? ` v${c.from} \u2192 v${c.to}` : c.to ? ` v${c.to}` : "";
2655
2947
  info(` ${icon} ${c.name}${versions} ${dim(c.action)}`);
2656
2948
  }
@@ -2665,9 +2957,9 @@ function projectOfFactory(projects2) {
2665
2957
  const sorted = [...projects2].sort((a, b) => b.length - a.length);
2666
2958
  return (cwd) => {
2667
2959
  if (!cwd) return null;
2668
- const known = sorted.find((p) => cwd === p || cwd.startsWith(p + sep5));
2960
+ const known = sorted.find((p) => cwd === p || cwd.startsWith(p + sep6));
2669
2961
  if (known) return known;
2670
- const repo = cwd.split(`${sep5}.claude${sep5}worktrees${sep5}`)[0];
2962
+ const repo = cwd.split(`${sep6}.claude${sep6}worktrees${sep6}`)[0];
2671
2963
  return isProjectCandidate(repo) ? repo : null;
2672
2964
  };
2673
2965
  }
@@ -2675,13 +2967,23 @@ async function usageBySkill(days, onlyProject) {
2675
2967
  const projectOf = projectOfFactory(onlyProject ? [onlyProject, ...knownProjects()] : knownProjects());
2676
2968
  const uses = await scanUsage(days);
2677
2969
  const scoped = onlyProject ? uses.filter((u) => projectOf(u.cwd) === onlyProject) : uses;
2678
- return new Map(summarize(scoped, projectOf).map((s) => [s.skill, s]));
2970
+ return new Map(summarize2(scoped, projectOf).map((s) => [s.skill, s]));
2679
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);
2680
2982
  function requireProject() {
2681
2983
  const root = projectHere();
2682
2984
  if (root) return root;
2683
2985
  if (!isProjectCandidate(findProjectRoot())) {
2684
- 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` : "";
2685
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.`);
2686
2988
  }
2687
2989
  throw new Error("Not in a git repo. Run this inside a project (or `git init` first).");
@@ -2704,14 +3006,8 @@ async function status(args) {
2704
3006
  `);
2705
3007
  if (skills.length === 0 && nested.length === 0) info("No skills in this project yet. Add one from your library with: skilllib add <name>");
2706
3008
  const usage2 = await usageBySkill(args.days, root);
2707
- const git2 = gitInfo(root, [...new Set(nested.map((s) => s.location))]);
2708
- const committed = (path) => ["committed", "changed"].includes(git2?.of(relativeTo(root, path)) ?? "");
2709
- const asRepo = {
2710
- "local only": "repo skill",
2711
- "untracked copy of library skill": "repo skill, in library",
2712
- "untracked, differs from library": "repo skill, differs from library"
2713
- };
2714
- 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));
2715
3011
  const agentsOf = (s) => {
2716
3012
  const ids = s.visibility.filter((v) => v.paths > 0).map((v) => v.id);
2717
3013
  return ids.length === s.visibility.length ? dim("all") : ids.length ? ids.map((id) => harness(id).name).join(", ") : red("none");
@@ -2776,7 +3072,7 @@ async function list(args) {
2776
3072
  } else {
2777
3073
  const installs = /* @__PURE__ */ new Map();
2778
3074
  for (const project of visibleProjects()) {
2779
- for (const name of Object.keys(readManifest(project).skills)) {
3075
+ for (const name of Object.keys(pinsIn(project))) {
2780
3076
  installs.set(name, [...installs.get(name) ?? [], basename9(project)]);
2781
3077
  }
2782
3078
  }
@@ -2795,7 +3091,7 @@ async function list(args) {
2795
3091
  );
2796
3092
  }
2797
3093
  const libraryNames = new Set(skills.map((s) => s.name));
2798
- 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))];
2799
3095
  if (globals.length > 0) {
2800
3096
  info("");
2801
3097
  warn(`${globals.length} of your global ${globals.length === 1 ? "skill still loads" : "skills still load"} in every project`);
@@ -2807,20 +3103,33 @@ async function list(args) {
2807
3103
  );
2808
3104
  }
2809
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
+ }
2810
3116
  function projects(args) {
2811
- 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
+ }
2812
3122
  if (args.json) {
2813
3123
  return json(
2814
3124
  "projects",
2815
- roots.map(
2816
- (root) => ({
3125
+ roots.map((root) => {
3126
+ const teamState = teamStateIn(root);
3127
+ return {
2817
3128
  name: basename9(root),
2818
3129
  path: root,
2819
- skills: projectStatus(root).map(
2820
- (s) => ({ name: s.name, managed: s.managed, state: s.state, version: s.version, latest: s.latest })
2821
- )
2822
- })
2823
- )
3130
+ skills: projectStatus(root).map((s) => ({ name: s.name, managed: s.managed, state: teamState(s), version: s.version, latest: s.latest }))
3131
+ };
3132
+ })
2824
3133
  );
2825
3134
  }
2826
3135
  if (roots.length === 0) {
@@ -2844,8 +3153,7 @@ function projects(args) {
2844
3153
  }
2845
3154
  function add(args) {
2846
3155
  if (args.positional.length === 0) {
2847
- error("Usage: skilllib add <skill-name>... [--force]");
2848
- process.exit(1);
3156
+ throw new Error("Usage: skilllib add <skill-name>... [--force]");
2849
3157
  }
2850
3158
  const root = requireProject();
2851
3159
  if (args.allowTracked) allowTrackedLinks(root);
@@ -2854,90 +3162,89 @@ function add(args) {
2854
3162
  }
2855
3163
  function remove(args) {
2856
3164
  if (args.positional.length === 0) {
2857
- error("Usage: skilllib remove <skill-name>... [--force]");
2858
- process.exit(1);
3165
+ throw new Error("Usage: skilllib remove <skill-name>... [--force]");
2859
3166
  }
2860
3167
  const root = requireProject();
2861
3168
  printChanges(root, args.positional.map((name) => removeSkill(root, name, { force: args.force })), args);
2862
3169
  }
2863
- function sync(args) {
2864
- const roots = args.all ? visibleProjects() : [requireProject()];
3170
+ function changeEach(args, change, nothing) {
3171
+ const roots = projectsFor(args);
2865
3172
  const results = roots.map((root) => {
2866
3173
  if (args.allowTracked) allowTrackedLinks(root);
2867
- const changes = syncProject(root, { force: args.force });
2868
- if (changes.some((c) => c.action === "skipped")) process.exitCode = 1;
3174
+ const changes = change(root);
3175
+ if (changes.some(incomplete)) process.exitCode = 1;
2869
3176
  if (!args.json) {
2870
3177
  if (args.all) info(`${basename9(root)} ${dim(tildify(root))}`);
2871
- if (changes.length === 0) {
2872
- if (existsSync13(join14(root, MANIFEST_FILE))) success("Installed versions match skilllib.json");
2873
- else info(dim("No skilllib.json: nothing to sync"));
2874
- } else printChanges(root, changes, args);
3178
+ if (changes.length === 0) nothing(root);
3179
+ else printChanges(root, changes, args);
2875
3180
  }
2876
3181
  return { project: basename9(root), path: root, changes: withDirs(root, changes) };
2877
3182
  });
2878
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
+ );
2879
3193
  if (!args.all) rememberProjects(roots);
2880
3194
  }
2881
3195
  function update(args) {
2882
- const roots = args.all ? visibleProjects() : [requireProject()];
2883
- const results = roots.map((root) => {
2884
- if (args.allowTracked) allowTrackedLinks(root);
2885
- const names = args.positional.length ? args.positional : void 0;
2886
- const changes = updateProject(root, names, { force: args.force });
2887
- if (!args.all) {
2888
- const { skills } = readManifest(root);
2889
- for (const name of names ?? []) if (!(name in skills)) changes.push({ name, action: "skipped", reason: "not managed by skilllib in this project" });
2890
- }
2891
- if (changes.some((c) => c.action === "skipped")) process.exitCode = 1;
2892
- if (!args.json) {
2893
- if (args.all) info(`${basename9(root)} ${dim(tildify(root))}`);
2894
- if (changes.length === 0) success("Everything is on the latest version");
2895
- else printChanges(root, changes, args);
2896
- }
2897
- return { project: basename9(root), path: root, changes: withDirs(root, changes) };
2898
- });
2899
- 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
+ );
2900
3208
  }
2901
3209
  function outdated(args) {
2902
- const roots = args.all ? visibleProjects() : [requireProject()];
3210
+ const roots = projectsFor(args);
2903
3211
  const behind = roots.flatMap(
2904
- (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 }))
2905
3213
  );
2906
3214
  if (args.json) return json("outdated", behind);
2907
- const rows = behind.map((b) => ({ Project: b.project, Skill: b.skill, Installed: `v${b.installed}`, Latest: `v${b.latest}`, Status: colorState(b.state) }));
2908
- if (rows.length === 0) return success("Everything is on the latest version");
2909
- 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);
2910
3218
  info(dim(`
2911
3219
  \u2192 skilllib update${args.all ? " --all" : ""}`));
2912
3220
  }
2913
3221
  function importSkills(args) {
2914
- 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));
2915
3224
  if (dirs.length === 0) {
2916
- error("Usage: skilllib import <skill-dir>... [--force] | skilllib import --global");
2917
- process.exit(1);
3225
+ throw new Error("Usage: skilllib import <skill-dir>... [--force] | skilllib import --global");
2918
3226
  }
2919
3227
  const results = [];
2920
3228
  for (const dir of dirs) {
2921
3229
  if (!isSkillDir(dir)) {
2922
3230
  warn(`${tildify(dir)}: no SKILL.md, skipping`);
2923
3231
  results.push({ path: dir, status: "no SKILL.md" });
3232
+ process.exitCode = 1;
2924
3233
  continue;
2925
3234
  }
2926
3235
  const { name, status: result } = importSkill(dir, { force: args.force });
2927
3236
  results.push({ path: dir, name, status: result });
2928
3237
  if (result === "exists") {
2929
3238
  warn(`${name}: the library already has a different version (use --force to replace it)`);
3239
+ process.exitCode = 1;
2930
3240
  continue;
2931
3241
  }
2932
3242
  success(`${name} ${dim(result === "unchanged" ? "already in library" : result === "added" ? "added to library" : "library updated")}`);
2933
- const skillsDir = resolve7(dir, "..");
2934
- const root = resolve7(skillsDir, "..", "..");
2935
- if (skillsDir === join14(root, ".claude", "skills") && isProjectCandidate(root)) {
2936
- if (readManifest(root).skills[name]?.hash !== treeHash(dir)) {
2937
- const change = addSkill(root, name);
2938
- if (change.action !== "skipped") info(dim(` ${basename9(root)} now depends on ${name} v${change.to}`));
2939
- }
2940
- 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}`));
2941
3248
  }
2942
3249
  }
2943
3250
  if (args.json) json("import", results);
@@ -2952,7 +3259,7 @@ function scan(args) {
2952
3259
  const found = discoverProjects();
2953
3260
  const library = new Set(librarySkills().map((s) => s.name));
2954
3261
  const skillNames = (root) => [
2955
- .../* @__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)])
2956
3263
  ];
2957
3264
  const withSkills = found.filter((root) => skillNames(root).length > 0);
2958
3265
  info(`Scanned ${roots.map((r) => tildify(r)).join(", ")}: ${found.length} project${found.length === 1 ? "" : "s"}, ${withSkills.length} with skills
@@ -2969,6 +3276,7 @@ ${yellow("Yellow")} skills aren't in your library yet (\`skilllib import <skill-
2969
3276
  }
2970
3277
  function folders(args) {
2971
3278
  const [action, ...paths] = args.positional;
3279
+ if (action && !["add", "remove", "rm"].includes(action)) throw new Error("Usage: skilllib folders [add|remove <folder>...]");
2972
3280
  let failed = false;
2973
3281
  if (action === "add") {
2974
3282
  for (const p of paths) {
@@ -2983,19 +3291,21 @@ function folders(args) {
2983
3291
  } else if (action === "remove" || action === "rm") {
2984
3292
  for (const p of paths) {
2985
3293
  if (removeRoot(p)) success(`Removed ${tildify(expandHome(p))}`);
2986
- 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
+ }
2987
3298
  }
2988
3299
  }
2989
3300
  const { roots, hidden } = readConfig();
2990
3301
  info(roots.length ? `Project folders:
2991
3302
  ${roots.map((r) => ` ${tildify(r)}`).join("\n")}` : "No project folders. Add one: skilllib folders add ~/Projects");
2992
3303
  if (hidden.length) info(dim(`Hidden projects: ${hidden.map((h) => tildify(h)).join(", ")} (skilllib unhide <path>)`));
2993
- if (failed) process.exit(1);
3304
+ if (failed) process.exitCode = 1;
2994
3305
  }
2995
3306
  function unhide(args) {
2996
3307
  if (args.positional.length === 0) {
2997
- error("Usage: skilllib unhide <path>...");
2998
- process.exit(1);
3308
+ throw new Error("Usage: skilllib unhide <path>...");
2999
3309
  }
3000
3310
  let failed = false;
3001
3311
  for (const p of args.positional) {
@@ -3007,20 +3317,18 @@ function unhide(args) {
3007
3317
  failed = true;
3008
3318
  }
3009
3319
  }
3010
- if (failed) process.exit(1);
3320
+ if (failed) process.exitCode = 1;
3011
3321
  }
3012
3322
  function newSkill(args) {
3013
3323
  const [name, ...rest] = args.positional;
3014
3324
  if (!name) {
3015
- error("Usage: skilllib new <name> [description]");
3016
- process.exit(1);
3325
+ throw new Error("Usage: skilllib new <name> [description]");
3017
3326
  }
3018
3327
  const r = createSkill(name, rest.join(" "));
3019
3328
  if (!r.ok) {
3020
- error(`Can't create ${name}: ${r.reason}`);
3021
- process.exit(1);
3329
+ throw new Error(`Can't create ${name}: ${r.reason}`);
3022
3330
  }
3023
- success(`Created ${tildify(join14(r.dir, "SKILL.md"))}`);
3331
+ success(`Created ${tildify(join15(r.dir, "SKILL.md"))}`);
3024
3332
  }
3025
3333
  function restore(args) {
3026
3334
  const backups = [...listBackups(), ...pluginBackups()];
@@ -3031,16 +3339,17 @@ function restore(args) {
3031
3339
  info(dim("\n\u2192 skilllib restore <name>"));
3032
3340
  return;
3033
3341
  }
3034
- const backup = backups.find((b) => b.name === name);
3035
- if (!backup) {
3036
- error(`No backup named ${name}`);
3037
- process.exit(1);
3038
- }
3039
- const r = backup.kind === "plugin" ? restorePlugin(backup) : restoreBackup(backup);
3040
- if (r.ok) success(`Restored ${name} to ${tildify(r.to)}`);
3041
- else {
3042
- error(`Can't restore ${name}: ${r.reason}`);
3043
- 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
+ }
3044
3353
  }
3045
3354
  }
3046
3355
  async function allIssues(args, withUsage = true) {
@@ -3052,11 +3361,14 @@ async function allIssues(args, withUsage = true) {
3052
3361
  const projectOf = projectOfFactory(projects2);
3053
3362
  const uses = await scanUsage(args.days);
3054
3363
  const inProject = usesByProject(uses, projectOf);
3055
- 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);
3056
3367
  issues.push(
3057
- ...usageIssues(projects2, machine, libraryNames, {
3368
+ ...usageIssues(readableRoots(projects2).readable, machine, libraryNames, {
3058
3369
  inProject: (root, skill) => inProject.get(root)?.get(skill) ?? 0,
3059
3370
  total: (skill) => total.get(skill) ?? 0,
3371
+ asUsed: (key) => asUsed.get(key) ?? 0,
3060
3372
  days: args.days
3061
3373
  })
3062
3374
  );
@@ -3101,7 +3413,7 @@ async function doctor(args) {
3101
3413
  async function apply(args) {
3102
3414
  const [id, pick] = args.positional;
3103
3415
  if (!id) throw new Error("Usage: skilllib apply <issue-id> [choice] (ids and choices: skilllib status --json or doctor --json)");
3104
- 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);
3105
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`);
3106
3418
  const optionsOf = (i) => i.choices ?? (i.fix ? [i.fix] : []);
3107
3419
  const labels = (i) => optionsOf(i).map((c) => c.label).join("\0");
@@ -3124,22 +3436,22 @@ async function apply(args) {
3124
3436
  else error(r.message);
3125
3437
  }
3126
3438
  async function usage(args) {
3127
- 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));
3128
3442
  if (args.json) {
3129
- const library2 = new Set(librarySkills().map((s) => s.name));
3130
3443
  return json("usage", {
3131
3444
  usageDays: args.days,
3132
3445
  source: "Claude Code transcripts",
3133
- 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) })),
3134
- 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
3135
3448
  });
3136
3449
  }
3137
3450
  if (summaries.length === 0) {
3138
3451
  info(`No skill usage in Claude Code transcripts from the last ${args.days} days.`);
3139
3452
  return;
3140
3453
  }
3141
- const library = new Set(librarySkills().map((s) => s.name));
3142
- 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"))})`)}
3143
3455
  `);
3144
3456
  table(
3145
3457
  summaries.map((s) => ({
@@ -3150,18 +3462,16 @@ async function usage(args) {
3150
3462
  Library: library.has(s.skill) ? green("yes") : dim("no")
3151
3463
  }))
3152
3464
  );
3153
- const unused = librarySkills().filter((s) => !summaries.some((u) => u.skill === s.name));
3154
3465
  if (unused.length > 0) {
3155
3466
  info("");
3156
- 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(", ")}`);
3157
3468
  }
3158
3469
  }
3159
3470
  function show(args) {
3160
3471
  const name = args.positional[0];
3161
- const dir = name ? join14(libraryDir(), name) : "";
3472
+ const dir = name ? join15(libraryDir(), name) : "";
3162
3473
  if (!name || !isSkillDir(dir)) {
3163
- error(name ? `${name} is not in the library` : "Usage: skilllib show <skill-name>");
3164
- process.exit(1);
3474
+ throw new Error(name ? `${name} is not in the library` : "Usage: skilllib show <skill-name>");
3165
3475
  }
3166
3476
  const { description } = readSkillInfo(dir);
3167
3477
  if (args.json) {
@@ -3171,14 +3481,14 @@ function show(args) {
3171
3481
  path: dir,
3172
3482
  latest: latestVersion(name)?.version ?? null,
3173
3483
  origin: libraryOrigins()[name] ?? "",
3174
- projects: knownProjects().filter((p) => name in readManifest(p).skills).map((p) => basename9(p)),
3175
- 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")
3176
3486
  });
3177
3487
  }
3178
3488
  info(`${name} ${dim(tildify(dir))}`);
3179
3489
  if (description) info(`
3180
3490
  ${description}`);
3181
- const using = knownProjects().filter((p) => name in readManifest(p).skills);
3491
+ const using = knownProjects().filter((p) => name in pinsIn(p));
3182
3492
  info(`
3183
3493
  Installed in: ${using.map((p) => tildify(p)).join(", ") || "no projects"}`);
3184
3494
  }
@@ -3187,8 +3497,7 @@ function harnessesCommand(args) {
3187
3497
  if (args.positional.length) {
3188
3498
  const unknown = args.positional.filter((p) => !ids.includes(p));
3189
3499
  if (unknown.length) {
3190
- error(`Unknown harness: ${unknown.join(", ")}. Choose from: ${ids.join(", ")}`);
3191
- process.exit(1);
3500
+ throw new Error(`Unknown harness: ${unknown.join(", ")}. Choose from: ${ids.join(", ")}`);
3192
3501
  }
3193
3502
  setHarnesses(args.positional);
3194
3503
  success(`Using ${args.positional.join(", ")}`);
@@ -3201,7 +3510,7 @@ function harnessesCommand(args) {
3201
3510
  New installs go to ${installDirs(enabled).join(" (real copy) + ")}${installDirs(enabled).length > 1 ? " (link)" : ""}.`));
3202
3511
  }
3203
3512
  function link(args) {
3204
- const roots = args.all ? visibleProjects() : [requireProject()];
3513
+ const roots = projectsFor(args);
3205
3514
  for (const root of roots) {
3206
3515
  if (args.allowTracked) allowTrackedLinks(root);
3207
3516
  const res = linkAll(root);
@@ -3212,6 +3521,7 @@ function link(args) {
3212
3521
  process.exitCode = 1;
3213
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`);
3214
3523
  }
3524
+ if (res.uncommitted.length) process.exitCode = 1;
3215
3525
  if (res.uncommitted.length)
3216
3526
  warn(
3217
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`
@@ -3220,19 +3530,17 @@ function link(args) {
3220
3530
  }
3221
3531
  function globalCommand(args) {
3222
3532
  const [action, ...names] = args.positional;
3223
- const yours = machineSkills().filter((m) => m.movable && !m.broken);
3533
+ const yours = yourGlobals();
3224
3534
  if (action === "move") return moveGlobal(args, names, yours);
3225
3535
  if (action === "keep" || action === "unkeep") {
3226
3536
  const unknown = names.filter((n) => !yours.some((m) => m.name === n));
3227
3537
  if (!names.length || unknown.length) {
3228
- error(names.length ? `Not one of your global skills: ${unknown.join(", ")}` : `Usage: skilllib global ${action} <name>...`);
3229
- process.exit(1);
3538
+ throw new Error(names.length ? `Not one of your global skills: ${unknown.join(", ")}` : `Usage: skilllib global ${action} <name>...`);
3230
3539
  }
3231
3540
  setKeepGlobal(names, action === "keep");
3232
3541
  success(action === "keep" ? `Keeping ${names.join(", ")} global on purpose` : `${names.join(", ")} will show up in cleanup again`);
3233
3542
  } else if (action) {
3234
- error("Usage: skilllib global [keep|unkeep <name>...] | skilllib global move <name>... --to <repo>...");
3235
- process.exit(1);
3543
+ throw new Error("Usage: skilllib global [keep|unkeep <name>...] | skilllib global move <name>... --to <repo>...");
3236
3544
  }
3237
3545
  const kept = keptGlobal();
3238
3546
  if (!yours.length) return info("None of your skills load globally.");
@@ -3247,7 +3555,7 @@ function repoRoot(repo) {
3247
3555
  if (named.length === 1) return named[0];
3248
3556
  if (named.length > 1) throw new Error(`Several projects are named ${repo}: ${named.map(tildify).join(", ")}. Pass its path.`);
3249
3557
  const path = expandHome(repo);
3250
- 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)`);
3251
3559
  const root = projectHere(path);
3252
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`);
3253
3561
  return root;
@@ -3259,6 +3567,7 @@ function moveGlobal(args, names, yours) {
3259
3567
  const roots = [...new Set(args.to.map(repoRoot))];
3260
3568
  const skills = yours.filter((m) => names.includes(m.name));
3261
3569
  const moves = moveGlobalSkills(skills, roots, { allowTracked: args.allowTracked });
3570
+ rememberProjects(roots);
3262
3571
  if (moves.some((m) => !m.moved)) process.exitCode = 1;
3263
3572
  if (args.json) {
3264
3573
  return json(
@@ -3274,10 +3583,7 @@ function moveGlobal(args, names, yours) {
3274
3583
  for (const m of moves) {
3275
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)`);
3276
3585
  else error(`${m.name}: ${m.reason}`);
3277
- for (const [root, c] of m.changes) {
3278
- if (c.blocked?.length) warn(` ${basename9(root)}: git tracks ${c.blocked.join(", ")}, so no link there. Re-run with --allow-tracked to add it.`);
3279
- 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.`);
3280
- }
3586
+ for (const [root, c] of m.changes) warnHeldLinks(c, `in ${basename9(root)}`);
3281
3587
  }
3282
3588
  if (moves.some((m) => m.moved)) info(dim("Commit skilllib.json and the skill folders in each repo."));
3283
3589
  }
@@ -3285,10 +3591,9 @@ function tidy(args) {
3285
3591
  const names = args.positional;
3286
3592
  const keepFor = (base) => args.keep ? Object.fromEntries(names.map((n) => [n, base ? resolve7(base, args.keep) : expandHome(args.keep)])) : {};
3287
3593
  if (args.keep && !names.length) {
3288
- error("Usage: skilllib tidy <name>... --keep <folder>");
3289
- process.exit(1);
3594
+ throw new Error("Usage: skilllib tidy <name>... --keep <folder>");
3290
3595
  }
3291
- const targets = args.global ? [null] : args.all ? visibleProjects() : [requireProject()];
3596
+ const targets = args.global ? [null] : projectsFor(args);
3292
3597
  const problems = [];
3293
3598
  for (const name of names) {
3294
3599
  const where = targets.map((root) => ({ root, ...foldersOf(root, name) })).filter((w) => w.all.length);
@@ -3306,8 +3611,7 @@ function tidy(args) {
3306
3611
  }
3307
3612
  }
3308
3613
  if (problems.length) {
3309
- for (const p of problems) error(p);
3310
- process.exit(1);
3614
+ throw new Error(problems.join("\n"));
3311
3615
  }
3312
3616
  const reports = targets.map(
3313
3617
  (root) => root === null ? { label: "global folders", report: planGlobalTidy({ keep: keepFor(null) }) } : { label: basename9(root), report: planProjectTidy(root, { keep: keepFor(root) }) }
@@ -3317,20 +3621,20 @@ function tidy(args) {
3317
3621
  for (const { label, report } of reports) {
3318
3622
  const plans = report.plans.filter((p) => !names.length || names.includes(p.name));
3319
3623
  const conflicts = report.conflicts.filter((c) => !names.length || names.includes(c.name));
3320
- 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)`);
3321
3625
  for (const plan of plans) {
3322
- const result = args.dryRun ? { applied: [], held: gitVisibleSteps(plan) } : applyTidy(plan, { git: args.allowGit ? "go" : "keep" });
3323
- const show2 = args.dryRun ? plan.steps : result.applied;
3324
- if (show2.length) info(`${args.dryRun ? dim("would") : green("\u2713")} ${label} \xB7 ${plan.name}: ${show2.map((s) => describeStep(s, plan.root)).join(", ")}`);
3325
- 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)}`);
3326
- 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;
3327
3631
  changed += result.applied.length;
3328
3632
  }
3329
3633
  if (args.keep && conflicts.length) process.exitCode = 1;
3330
3634
  for (const c of conflicts) {
3331
3635
  warn(`${label} \xB7 ${c.name}: the copies differ`);
3332
3636
  for (const copy of c.copies) {
3333
- const runs = copy.runs.map((id) => HARNESSES.find((h) => h.id === id).name);
3637
+ const runs = copy.runs.map((id) => harness(id).name);
3334
3638
  info(` ${copyLabel(c, copy.dir)}${runs.length ? dim(` run by ${runs.join(", ")}`) : ""}${c.managed === copy.dir ? dim(" (skilllib's copy)") : ""}`);
3335
3639
  }
3336
3640
  const pick = copyLabel(c, c.managed ?? c.copies[0].dir);
@@ -3347,8 +3651,7 @@ function agentSkill(args) {
3347
3651
  if (action === "install") {
3348
3652
  const r = installAgentSkill();
3349
3653
  if (!r.ok) {
3350
- error(`Can't install the skilllib skill: ${r.reason}`);
3351
- process.exit(1);
3654
+ throw new Error(`Can't install the skilllib skill: ${r.reason}`);
3352
3655
  }
3353
3656
  if (args.json) return json("agent-skill", { state: agentSkillState(), dirs: r.dirs });
3354
3657
  success(`Installed in ${r.dirs.map((d) => tildify(d)).join(" and ")}`);
@@ -3361,6 +3664,7 @@ function agentSkill(args) {
3361
3664
  if (args.json) return json("agent-skill", { state: agentSkillState(), removed });
3362
3665
  return removed.length ? success(`Removed from ${removed.map((d) => tildify(d)).join(" and ")}`) : info("The skilllib skill isn't installed.");
3363
3666
  }
3667
+ if (action) throw new Error("Usage: skilllib agent-skill [install|remove]");
3364
3668
  const state = agentSkillState();
3365
3669
  if (args.json) return json("agent-skill", { state, dirs: agentSkillDirs() });
3366
3670
  if (state === "installed") success(`Your agents can use skilllib ${dim(`(${agentSkillDirs().map((d) => tildify(d)).join(", ")})`)}`);
@@ -3374,14 +3678,19 @@ export {
3374
3678
  HARNESSES,
3375
3679
  harness,
3376
3680
  readSkillInfo,
3681
+ treeHash,
3377
3682
  versionHistory,
3378
3683
  forgetLatest,
3379
3684
  latestVersion,
3380
3685
  projectHere,
3381
3686
  readManifest,
3687
+ readableRoots,
3382
3688
  setJsonMode,
3689
+ isJsonMode,
3690
+ json,
3383
3691
  error,
3384
3692
  tildify,
3693
+ localStamp,
3385
3694
  readConfig,
3386
3695
  addRoot,
3387
3696
  removeRoot,
@@ -3406,43 +3715,34 @@ export {
3406
3715
  relinkDependency,
3407
3716
  addSkill,
3408
3717
  removeSkill,
3409
- syncProject,
3410
3718
  updateProject,
3411
3719
  importSkill,
3412
3720
  removeUntracked,
3413
3721
  projectsUsing,
3414
3722
  deleteLibrarySkill,
3415
3723
  deleteGlobal,
3416
- unloadGlobal,
3417
- linkGlobal,
3418
3724
  createSkill,
3419
3725
  listBackups,
3420
3726
  restoreBackup,
3421
3727
  gitInfo,
3422
3728
  relativeTo,
3423
- claudePlugins,
3424
3729
  installedPlugins,
3425
3730
  cursorPlugins,
3426
3731
  forgetClaudeBinary,
3427
3732
  claudeBinary,
3428
- turnOffIn,
3429
3733
  setPluginOn,
3430
3734
  uninstallPlugin,
3431
3735
  updatePlugin,
3432
3736
  pluginBackups,
3433
3737
  restorePlugin,
3434
- cursorPluginSkills,
3435
3738
  agentSkillState,
3436
3739
  installAgentSkill,
3437
3740
  removeAgentSkill,
3438
3741
  refreshAgentSkill,
3439
- planProjectTidy,
3440
- planGlobalTidy,
3441
- gitVisibleSteps,
3442
- applyTidy,
3443
- describeStep,
3444
- copyLabel,
3445
- addedAt,
3742
+ foldersOf,
3743
+ runFix,
3744
+ findIssues,
3745
+ usageIssues,
3446
3746
  scanUsage,
3447
3747
  moveGlobalSkills,
3448
3748
  parseArgs,