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.
@@ -3,25 +3,20 @@ import {
3
3
  PROJECT_SKILLS_DIR,
4
4
  addRoot,
5
5
  addSkill,
6
- addedAt,
7
6
  agentSkillState,
8
7
  allowTrackedLinks,
9
- applyTidy,
10
8
  claudeBinary,
11
- claudePlugins,
12
- copyLabel,
13
9
  createSkill,
14
- cursorPluginSkills,
15
10
  cursorPlugins,
16
11
  deleteGlobal,
17
12
  deleteLibrarySkill,
18
- describeStep,
19
13
  discoverProjects,
20
14
  enabledHarnesses,
15
+ findIssues,
16
+ foldersOf,
21
17
  forgetClaudeBinary,
22
18
  forgetLatest,
23
19
  gitInfo,
24
- gitVisibleSteps,
25
20
  harness,
26
21
  harnessesChosen,
27
22
  importSkill,
@@ -33,12 +28,10 @@ import {
33
28
  librarySkillDir,
34
29
  librarySkills,
35
30
  linkEverywhere,
36
- linkGlobal,
37
31
  listBackups,
32
+ localStamp,
38
33
  machineSkills,
39
34
  moveGlobalSkills,
40
- planGlobalTidy,
41
- planProjectTidy,
42
35
  pluginBackups,
43
36
  projectHere,
44
37
  projectOfFactory,
@@ -47,6 +40,7 @@ import {
47
40
  readConfig,
48
41
  readManifest,
49
42
  readSkillInfo,
43
+ readableRoots,
50
44
  recordOrigin,
51
45
  relativeTo,
52
46
  relinkDependency,
@@ -56,6 +50,7 @@ import {
56
50
  removeUntracked,
57
51
  restoreBackup,
58
52
  restorePlugin,
53
+ runFix,
59
54
  scanUsage,
60
55
  setHarnesses,
61
56
  setHidden,
@@ -63,23 +58,22 @@ import {
63
58
  setPluginOn,
64
59
  skilllibHome,
65
60
  skillsLoadedIn,
66
- syncProject,
67
61
  tildify,
68
- turnOffIn,
62
+ treeHash,
69
63
  uninstallPlugin,
70
- unloadGlobal,
71
64
  updatePlugin,
72
65
  updateProject,
66
+ usageIssues,
73
67
  versionHistory,
74
68
  visibilityOf,
75
69
  visibleProjects
76
- } from "./chunk-LO4FMXDE.js";
70
+ } from "./chunk-3XDWEHI4.js";
77
71
 
78
72
  // src/tui/index.tsx
79
73
  import { render } from "ink";
80
74
 
81
75
  // src/tui/App.tsx
82
- import { useEffect, useRef, useState } from "react";
76
+ import { useEffect, useMemo, useRef, useState } from "react";
83
77
  import { Box as Box2, Text as Text2, useApp, useInput, useWindowSize } from "ink";
84
78
  import { spawnSync as spawnSync2 } from "child_process";
85
79
  import { existsSync as existsSync2 } from "fs";
@@ -135,7 +129,6 @@ function ListPanel({
135
129
  rows,
136
130
  selected,
137
131
  empty,
138
- filter,
139
132
  header,
140
133
  headerHeight = 0
141
134
  }) {
@@ -143,7 +136,7 @@ function ListPanel({
143
136
  const offset = Math.min(Math.max(0, selected - Math.floor(visible / 2)), Math.max(0, rows.length - visible));
144
137
  const shown = rows.slice(offset, offset + visible);
145
138
  const inner = width - 4;
146
- const hint = [filter ?? "", rows.length > visible ? `${selected + 1}/${rows.length}` : ""].filter(Boolean).join(" ");
139
+ const hint = rows.length > visible ? `${selected + 1}/${rows.length}` : "";
147
140
  return /* @__PURE__ */ jsxs(Panel, { title, hint, focused, width, height, children: [
148
141
  header,
149
142
  rows.length === 0 ? /* @__PURE__ */ jsx(Box, { paddingTop: 1, paddingX: 1, children: typeof empty === "string" ? /* @__PURE__ */ jsx(Text, { color: color.muted, children: empty }) : empty }) : shown.map((row, i) => {
@@ -166,7 +159,6 @@ function ListPanel({
166
159
  backgroundColor: bg,
167
160
  color: c.color ?? (active && focused ? color.text : void 0),
168
161
  bold: c.bold || active && focused && c.grow,
169
- dimColor: c.dim,
170
162
  wrap: "truncate-end",
171
163
  children: fit(c.text, c.grow ? growWidth : c.width ?? c.text.length)
172
164
  },
@@ -199,7 +191,6 @@ import { spawnSync } from "child_process";
199
191
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
200
192
  import { join } from "path";
201
193
  var EXCERPT_CHARS = 1200;
202
- var tildify2 = (p) => tildify(p);
203
194
  function skillExcerpt(dir, chars) {
204
195
  const file = join(dir, "SKILL.md");
205
196
  if (!existsSync(file)) return { text: "(SKILL.md not found)", files: 0, size: 0 };
@@ -223,7 +214,7 @@ function buildReviewPrompt(skills, ctx) {
223
214
  `- Description: ${s.description || "(none)"}`,
224
215
  `- Source: ${s.source}`,
225
216
  `- Installed via: ${s.installedHow}`,
226
- `- Location: ${tildify2(s.path)}${s.links.length ? ` (also linked from ${s.links.map(tildify2).join(", ")})` : ""}`,
217
+ `- Location: ${tildify(s.path)}${s.links.length ? ` (also linked from ${s.links.map(tildify).join(", ")})` : ""}`,
227
218
  `- Loaded by: ${s.loadedBy}`,
228
219
  ...s.vendor ? [`- Vendor-managed: ${s.vendor}`] : [],
229
220
  ...s.keptGlobal ? ["- I marked this as global on purpose: recommend changing that only for a strong reason (unused, duplicated, low quality)"] : [],
@@ -249,7 +240,7 @@ function buildReviewPrompt(skills, ctx) {
249
240
  - Usage counts only cover Claude Code sessions over the last ${ctx.usageDays} days; other agents' usage isn't tracked, so treat "unused" as a strong hint, not proof.
250
241
 
251
242
  ## My projects
252
- ${ctx.projects.map((p) => `- ${p.name} \u2014 ${tildify2(p.path)}`).join("\n") || "(none)"}
243
+ ${ctx.projects.map((p) => `- ${p.name} \u2014 ${tildify(p.path)}`).join("\n") || "(none)"}
253
244
 
254
245
  You can open the paths above (skills and projects) to check what a skill does and what each project uses (package.json, README, tech stack).
255
246
 
@@ -308,7 +299,65 @@ var joined = (results) => {
308
299
  const message = results.map(said).join(" \xB7 ");
309
300
  return results.some(isFailure) ? failed(message) : message;
310
301
  };
311
- var SEVERITY_RANK = { problem: 0, warning: 1, hint: 2 };
302
+ var SEVERITY_RANK = { problem: 0, suggestion: 1 };
303
+ function fixOf(c, found) {
304
+ return {
305
+ label: c.label,
306
+ preview: c.hint ? `${c.hint[0].toUpperCase()}${c.hint.slice(1)}. ${found.detail}` : found.detail,
307
+ run: () => {
308
+ const r = runFix(c);
309
+ return r.ok ? r.message : failed(r.message);
310
+ }
311
+ };
312
+ }
313
+ function shortOf(found) {
314
+ const kind = found.id.split(":")[0];
315
+ if (found.id === "unused:library") return "In no repo, unused 30 days";
316
+ if (kind === "plugin") return `Also in \u29C9 ${found.plugin.split("@")[0]}`;
317
+ if (kind === "cursor-plugin") return `Also in Cursor's ${found.plugin}`;
318
+ const short = {
319
+ broken: "Broken link",
320
+ dup: "Loaded twice",
321
+ tidy: "Extra copies",
322
+ conflict: "Copies differ",
323
+ review: "Not reviewed",
324
+ usable: "Not every agent can use it",
325
+ sync: "Folder missing",
326
+ unavailable: "Pinned, not in your library",
327
+ pin: "Pinned version not in your library",
328
+ update: "Out of date",
329
+ uncommitted: "Commit it first",
330
+ local: "Not in your library",
331
+ adopt: "Not tracked",
332
+ differs: "Differs from your library",
333
+ npx: "From npx skills",
334
+ "plugin-update": "Update available",
335
+ "plugin-unused": "Unused 30 days",
336
+ edited: "Edited here",
337
+ "not-in-lib": "No longer in your library",
338
+ twice: "Repo and global copies",
339
+ unused: "Unused 30 days"
340
+ };
341
+ return short[kind] ?? found.title;
342
+ }
343
+ var views = /* @__PURE__ */ new WeakMap();
344
+ function issueOf(found) {
345
+ let view = views.get(found);
346
+ if (!view) views.set(found, view = viewOf(found));
347
+ return view;
348
+ }
349
+ function viewOf(found) {
350
+ return {
351
+ id: found.id,
352
+ severity: found.severity,
353
+ title: found.title,
354
+ short: shortOf(found),
355
+ detail: found.detail,
356
+ skills: [...new Set(found.at?.map((a) => a.skill))],
357
+ decision: !found.fix,
358
+ fixes: (found.fix ? [found.fix] : found.choices ?? []).map((c) => fixOf(c, found))
359
+ };
360
+ }
312
361
  function filterOf(source) {
313
362
  return source === "lib" || source === "repo" || source === "untracked" ? "local" : source === "global" || source === "skilllib" ? "global" : source === "plugin" ? "plugins" : "vendor";
314
363
  }
@@ -337,63 +386,25 @@ function usable(w, projectName) {
337
386
  ...machineIn(w, p).map((m) => ({ name: m.name, source: m.source, where: m.where, agents: m.agents, uses: uses(m.name), machine: m }))
338
387
  ];
339
388
  }
340
- function lib(w, name) {
341
- return w.library.find((l) => l.name === name);
389
+ function issuesAt(w, repo, skill) {
390
+ const root = repo === null ? null : project(w, repo).path;
391
+ return w.issues.filter((i) => i.at?.some((a) => a.root === root && a.skill === skill)).map(issueOf);
342
392
  }
343
- var agentNames = (ids) => ids.map((a) => harness(a).name).join(", ");
344
- function usedElsewhere(w, except, name) {
345
- const repos = w.projects.filter((p) => p.name !== except && (w.usage[p.name]?.[name] ?? 0) > 0 && !p.skills.some((s) => s.name === name)).map((p) => p.name);
346
- return repos.length ? `, and ${repos.join(", ")} used it in the last 30 days` : "";
393
+ function repoIssues(w, repo) {
394
+ const root = project(w, repo).path;
395
+ return w.issues.filter((i) => i.at?.some((a) => a.root === root)).map(issueOf);
347
396
  }
348
- function dupeIssues(repo, name, d) {
349
- const g = repo === null ? "-g" : "";
350
- if (d.differ)
351
- return [
352
- {
353
- id: `conflict${g}:${name}`,
354
- severity: "problem",
355
- title: `Copies differ: ${d.differ.map((c) => c.label + (c.runs.length ? ` (${agentNames(c.runs)})` : "")).join(" vs ")}`,
356
- short: "Copies differ",
357
- decision: true,
358
- fixes: d.differ.filter((c) => c.canWin).map((c) => ({
359
- label: `Keep the ${c.label} copy`,
360
- preview: `The other ${d.differ.length === 2 ? "copy becomes a link" : "copies become links"} to it, so every agent runs this one. Replaced copies go to Settings \u203A Backups.${repo ? " If git tracks a copy that changes, skilllib asks first." : ""}`,
361
- run: (w) => w.ops.tidy(repo, name, { keep: c.dir })
362
- }))
363
- }
364
- ];
365
- return [
366
- {
367
- id: `copies${g}:${name}`,
368
- severity: "warning",
369
- title: repo ? "More copies or links in this repo than your agents need" : "Identical copies in several global folders",
370
- short: "Extra copies",
371
- decision: false,
372
- fixes: [
373
- {
374
- label: "Keep one copy, plus the links your agents need",
375
- preview: `${d.steps.join("; ")}. Replaced copies go to Settings \u203A Backups.${d.git.length ? ` Git would see ${d.git.join(", ")}: skilllib asks before ${d.git.length === 1 ? "that one" : "those"}.` : ""}`,
376
- run: (w) => w.ops.tidy(repo, name)
377
- }
378
- ]
379
- }
380
- ];
397
+ function unlisted(w, repo) {
398
+ const root = project(w, repo).path;
399
+ const listed = new Set(usable(w, repo).map((u) => u.name));
400
+ return [...new Set(w.issues.flatMap((i) => (i.at ?? []).filter((a) => a.root === root && !listed.has(a.skill)).map((a) => a.skill)))];
381
401
  }
382
- function cursorPluginIssue(name, plugin) {
383
- return {
384
- id: `cursor-plugin:${name}`,
385
- severity: "warning",
386
- title: `Also in the Cursor plugin ${plugin}: Cursor lists both while it's on`,
387
- short: "Also in a Cursor plugin",
388
- decision: true,
389
- fixes: [
390
- {
391
- label: "Turn the plugin off in Cursor (Settings \u203A Plugins)",
392
- preview: "skilllib can't read or change Cursor's plugins. Your skill wins once the plugin is off.",
393
- run: () => `Turn ${plugin} off in Cursor; skilllib can't tell whether it's on`
394
- }
395
- ]
396
- };
402
+ function libraryIssues(w, name) {
403
+ return w.issues.filter((i) => i.id === "unused:library" && i.at?.some((a) => a.skill === name)).map(issueOf);
404
+ }
405
+ function pluginIssues(w, p) {
406
+ const id = p.agent === "cursor" ? pluginName(p) : p.id;
407
+ return w.issues.filter((i) => i.plugin === id && (i.pluginAt === void 0 || i.pluginAt === (p.projectPath ?? null))).map(issueOf);
397
408
  }
398
409
  function globalFixes(m) {
399
410
  return [
@@ -403,7 +414,7 @@ function globalFixes(m) {
403
414
  run: () => "",
404
415
  pickRepos: (w, repos) => w.ops.moveGlobal([m], repos)
405
416
  },
406
- m.kept ? { label: "Stop marking it as global on purpose", preview: `skilllib warns about ${m.name} again, like any global skill.`, run: (w) => w.ops.keepGlobal(m.name, false) } : { label: "Keep it global on purpose", preview: `Mark ${m.name} as global on purpose. skilllib stops warning about it.`, run: (w) => w.ops.keepGlobal(m.name, true) },
417
+ m.kept ? { label: "Stop marking it as global on purpose", preview: `${m.name} shows as Not reviewed again, like any global skill.`, run: (w) => w.ops.keepGlobal(m.name, false) } : { label: "Keep it global on purpose", preview: `Mark ${m.name} as global on purpose: it no longer shows as Not reviewed. Its other issues stay.`, run: (w) => w.ops.keepGlobal(m.name, true) },
407
418
  { label: "Delete it", preview: `Remove ${m.path}. A backup is kept in Settings \u203A Backups.`, run: (w) => w.ops.deleteGlobal(m) }
408
419
  ];
409
420
  }
@@ -411,267 +422,6 @@ function machineActions(m) {
411
422
  if (m.broken) return [];
412
423
  return m.source === "global" ? globalFixes(m) : m.source === "skilllib" ? [removeAgentSkillFix()] : [];
413
424
  }
414
- function machineIssues(w, m) {
415
- if (m.source === "skilllib") return [agentSkillIssue(w)].flatMap((i) => i ? [i] : []);
416
- if (m.broken)
417
- return [
418
- {
419
- id: `broken:${m.path}`,
420
- severity: "problem",
421
- title: "Broken link: points at a folder that no longer exists",
422
- short: "Broken link",
423
- decision: false,
424
- fixes: [{ label: "Remove the link", preview: `Remove ${m.path}.`, run: (w2) => w2.ops.deleteGlobal(m) }]
425
- }
426
- ];
427
- const issues = [];
428
- const other = w.machine.find((x) => x !== m && x.name === m.name && !x.broken && x.agents.some((a) => m.agents.includes(a)));
429
- if (m.source === "global" && other && other.source !== "global")
430
- issues.push({
431
- id: `dup-plugin:${m.name}`,
432
- severity: "problem",
433
- title: `Loaded twice: also in ${other.source} ${other.where}`,
434
- short: "Loaded twice",
435
- // Every repo then runs the other copy instead of yours: a decision, never "fix all".
436
- decision: true,
437
- fixes: [
438
- {
439
- label: `Keep the ${other.source}'s copy, stop loading yours globally`,
440
- preview: `Copy ${m.name} into your library if needed, then move ${m.path} to the backups. Every repo then runs the ${other.source}'s copy instead of yours.`,
441
- run: (w2) => w2.ops.unloadGlobal(m)
442
- }
443
- ]
444
- });
445
- if (m.source === "global" && m.dupes) issues.push(...dupeIssues(null, m.name, m.dupes));
446
- if (m.source === "global" && m.cursorPlugin && m.agents.includes("cursor")) issues.push(cursorPluginIssue(m.name, m.cursorPlugin));
447
- const seen = new Set(w.machine.filter((x) => x.name === m.name && !x.broken).flatMap((x) => x.agents));
448
- const blind = m.source === "global" && m.agents.length && !m.dupes ? w.agents.filter((a) => !seen.has(a)) : [];
449
- const names = blind.map((a) => harness(a).name).join(", ");
450
- const link = {
451
- label: `Link it for ${names}`,
452
- preview: `Add a link in the global skills folder ${names} read${blind.length === 1 ? "s" : ""} (nothing is copied or moved), so ${names} load${blind.length === 1 ? "s" : ""} it in every repo too.`,
453
- run: (w2) => w2.ops.linkGlobal(m, blind)
454
- };
455
- if (m.kept && blind.length)
456
- issues.push({ id: `blind-global:${m.name}`, severity: "warning", title: `${names} can't see it: only ${m.agents.map((a) => harness(a).name).join(", ")} load${m.agents.length === 1 ? "s" : ""} it`, short: `${names} can't see it`, decision: false, fixes: [link] });
457
- if (m.source === "global" && !m.kept) {
458
- const [move, ...rest] = globalFixes(m);
459
- issues.push({
460
- id: `global:${m.name}`,
461
- severity: "warning",
462
- title: blind.length ? `Global, not reviewed: loads in every repo, but ${names} can't see it` : "Global, not reviewed: loads in every repo",
463
- short: blind.length ? `Not reviewed \xB7 ${names} can't see it` : "Not reviewed",
464
- decision: true,
465
- fixes: blind.length ? [move, link, ...rest] : [move, ...rest]
466
- });
467
- }
468
- return issues;
469
- }
470
- function issuesOf(w, projectName, u) {
471
- const s = u.local;
472
- const issues = [];
473
- const shares = (m) => m.name === s.name && !m.broken && m.agents.some((a) => s.agents.includes(a));
474
- const loaded = machineIn(w, project(w, projectName));
475
- const g = loaded.find((m) => m.source === "global" && shares(m));
476
- const vendor = loaded.find((m) => m.source !== "global" && shares(m));
477
- const l = lib(w, s.name);
478
- const remove = {
479
- label: "Remove this repo's copy",
480
- preview: s.source === "lib" ? `Delete ${s.dir}/${s.name} in ${projectName}. Your library keeps it.` : `Move ${s.dir}/${s.name} in ${projectName} to Settings \u203A Backups.`,
481
- run: (w2) => w2.ops.remove(projectName, s.name)
482
- };
483
- if (s.missing && s.library === "missing")
484
- issues.push({
485
- id: `unavailable:${s.name}`,
486
- severity: "problem",
487
- title: "Pinned in skilllib.json, but not in your library: import it, or set SKILLLIB_HOME to the library that has it",
488
- short: "Pinned, not in your library",
489
- decision: true,
490
- fixes: [{ label: "Remove it from skilllib.json", preview: `${projectName}'s skilllib.json stops pinning ${s.name} v${s.version}. There's no folder to delete.`, run: (w2) => w2.ops.remove(projectName, s.name) }]
491
- });
492
- else if (s.missing)
493
- issues.push({
494
- id: `missing:${s.name}`,
495
- severity: "problem",
496
- title: "Listed in skilllib.json but the folder is missing",
497
- short: "Folder missing",
498
- decision: false,
499
- fixes: [{ label: "Restore it (sync)", preview: `Reinstall ${s.name} v${s.version} into ${s.dir}.`, run: (w2) => w2.ops.restore(projectName, s.name) }]
500
- });
501
- else if (s.source === "lib" && s.library === "missing")
502
- issues.push({
503
- id: `not-in-lib:${s.name}`,
504
- severity: "problem",
505
- title: "Tracked here, but no longer in your library",
506
- short: "Not in your library",
507
- decision: false,
508
- fixes: [{ label: "Copy it back into your library", preview: `Copy this repo's ${s.name} into your library, so it can be restored and shared again.`, run: (w2) => w2.ops.copyToLibrary(projectName, s.name) }]
509
- });
510
- if (g?.kept)
511
- issues.push({
512
- id: `kept-g:${s.name}`,
513
- severity: "warning",
514
- title: `Also global in ${g.where}, and you keep it global: this repo's copy is extra`,
515
- short: "Extra: you keep it global",
516
- decision: true,
517
- fixes: [
518
- ...s.source !== "repo" ? [{ ...remove, label: "Remove this repo's copy, keep it global" }] : [],
519
- {
520
- label: "Stop loading it globally after all",
521
- preview: `Copy ${s.name} into your library if needed, then move ${g.path} to the backups. Every other repo stops seeing it${usedElsewhere(w, projectName, s.name)}.`,
522
- run: (w2) => w2.ops.unloadGlobal(g)
523
- }
524
- ]
525
- });
526
- else if (g)
527
- issues.push({
528
- id: `twice-g:${s.name}`,
529
- severity: "problem",
530
- title: `Loaded twice: also global in ${g.where}`,
531
- short: "Loaded twice",
532
- // Unloading the global copy changes every repo: a decision, never "fix all".
533
- decision: true,
534
- fixes: [
535
- {
536
- label: "Keep this repo's copy, stop loading it globally",
537
- preview: `Copy ${s.name} into your library if needed, then move ${g.path} to the backups. Every other repo stops seeing it${usedElsewhere(w, projectName, s.name)}.`,
538
- run: (w2) => w2.ops.unloadGlobal(g)
539
- },
540
- ...s.source !== "repo" ? [{ ...remove, label: "Keep it global, remove it from this repo" }] : []
541
- ]
542
- });
543
- const cursorOnly = vendor?.source === "plugin" && w.agents.includes("claude-code") && !vendor.agents.includes("claude-code");
544
- if (vendor && cursorOnly)
545
- issues.push({
546
- id: `twice-p:${s.name}`,
547
- severity: "warning",
548
- title: `Same name as a skill in plugin ${vendor.where}: Claude Code has it off in ${projectName}, but Cursor ignores repo settings and lists both`,
549
- short: "Cursor also lists a plugin's copy",
550
- decision: true,
551
- fixes: [pluginFixFor(w, vendor.where)]
552
- });
553
- else if (vendor)
554
- issues.push({
555
- id: `twice-p:${s.name}`,
556
- severity: "problem",
557
- title: `Same name as a skill in ${vendor.source} ${vendor.where}`,
558
- short: `Same name as a ${vendor.source} skill`,
559
- decision: true,
560
- fixes: [
561
- ...s.source !== "repo" ? [{ ...remove, label: `Remove this repo's copy, use the ${vendor.source}'s` }] : [],
562
- vendor.source === "plugin" ? {
563
- label: `Turn the plugin off in ${projectName} only`,
564
- preview: `Sets "${vendor.where}": false in ${projectName}'s .claude/settings.local.json (claude plugin disable --scope local), a local-only file git ignores. Other repos keep it.${vendor.agents.includes("cursor") ? " Cursor ignores repo settings, so it still lists both." : ""}`,
565
- run: (w2) => w2.ops.pluginOffHere(projectName, vendor.where)
566
- } : {
567
- label: `Turn it off in ${vendor.where}`,
568
- preview: "skilllib can't turn it off for you.",
569
- run: () => `Turn ${vendor.where} off at its source; skilllib picks it up next time`
570
- }
571
- ]
572
- });
573
- if (s.dupes) issues.push(...dupeIssues(projectName, s.name, s.dupes));
574
- if (s.cursorPlugin && s.agents.includes("cursor")) issues.push(cursorPluginIssue(s.name, s.cursorPlugin));
575
- if (s.source === "lib" && s.edited)
576
- issues.push({
577
- id: `edited:${s.name}`,
578
- severity: "warning",
579
- title: `Edited here: differs from library v${s.version}`,
580
- short: "Edited here",
581
- decision: true,
582
- fixes: [
583
- { label: `Save as v${(l?.latest ?? 0) + 1} in your library`, preview: `Your library gets v${(l?.latest ?? 0) + 1} with these edits. Other repos can update to it.`, run: (w2) => w2.ops.saveEdits(projectName, s.name) },
584
- { label: "Discard the edits", preview: `Replace ${s.dir}/${s.name} with library v${s.version} (edits backed up).`, run: (w2) => w2.ops.discardEdits(projectName, s.name) }
585
- ]
586
- });
587
- if (s.source === "lib" && l && s.version < l.latest && !s.edited && !s.missing)
588
- issues.push({
589
- id: `outdated:${s.name}`,
590
- severity: "warning",
591
- title: `Update available: v${s.version} \u2192 v${l.latest}`,
592
- short: `Update to v${l.latest}`,
593
- decision: false,
594
- fixes: [{ label: `Update to v${l.latest}`, preview: `Replace ${s.dir}/${s.name} with library v${l.latest}.`, run: (w2) => w2.ops.update(projectName, s.name) }]
595
- });
596
- const blind = w.agents.filter((a) => !u.agents.includes(a));
597
- const unshared = blind.filter((a) => s.unshared?.includes(a));
598
- const linkable = blind.filter((a) => !unshared.includes(a));
599
- const commitFirst = unshared.length ? ` ${agentNames(unshared)} ${unshared.length === 1 ? "needs" : "need"} it committed first (git tracks the folder they read).` : "";
600
- if (!s.missing && linkable.length)
601
- issues.push({
602
- id: `blind:${s.name}`,
603
- severity: "warning",
604
- title: `${agentNames(blind)} can't see it`,
605
- short: `${agentNames(blind)} can't see it`,
606
- decision: false,
607
- fixes: [
608
- {
609
- label: "Link it for every agent",
610
- preview: `Add links (nothing is copied or moved), so ${agentNames(linkable)} ${linkable.length === 1 ? "loads" : "load"} it too.${commitFirst}`,
611
- run: (w2) => w2.ops.link(projectName, s.name)
612
- }
613
- ]
614
- });
615
- else if (!s.missing && unshared.length)
616
- issues.push({
617
- id: `blind:${s.name}`,
618
- severity: "warning",
619
- title: `${agentNames(unshared)} can't see it: commit ${s.dir}/${s.name} first, then link it (git tracks the folder they read, and a link to an uncommitted copy would break for teammates)`,
620
- short: `${agentNames(unshared)} can't see it: commit it first`,
621
- // Nothing skilllib can do: committing is yours.
622
- decision: true,
623
- fixes: []
624
- });
625
- const differ = !!s.dupes?.differ;
626
- if (s.source !== "lib" && s.library === "differs" && !differ)
627
- issues.push({
628
- id: `differs:${s.name}`,
629
- severity: "hint",
630
- title: "Differs from the copy in your library",
631
- short: "Differs from library",
632
- decision: true,
633
- fixes: [
634
- { label: "Update your library from this copy", preview: `Your library gets a new version of ${s.name} with this repo's content.`, run: (w2) => w2.ops.copyToLibrary(projectName, s.name) },
635
- ...s.source === "untracked" ? [{ label: "Replace it with the library version", preview: `Replace ${s.dir}/${s.name} with your library's newest version and track it (this copy backed up).`, run: (w2) => w2.ops.discardEdits(projectName, s.name) }] : []
636
- ]
637
- });
638
- if (s.source === "untracked" && s.library === "same" && !differ)
639
- issues.push({
640
- id: `adopt:${s.name}`,
641
- severity: "hint",
642
- title: "Same as your library skill, but not tracked",
643
- short: "Not tracked",
644
- // It writes to the repo's skilllib.json (and your library): your call, never in Fix all (as in doctor, #11).
645
- decision: true,
646
- fixes: [{ label: "Track it", preview: `Record ${s.name} in skilllib.json so library updates reach it.`, run: (w2) => w2.ops.track(projectName, s.name) }]
647
- });
648
- if (s.source === "untracked" && !l && !vendor && !differ)
649
- issues.push({
650
- id: `local:${s.name}`,
651
- severity: "hint",
652
- title: "Only exists in this repo",
653
- short: "Only in this repo",
654
- // It writes to the repo's skilllib.json (and your library): your call, never in Fix all (as in doctor, #11).
655
- decision: true,
656
- fixes: [
657
- {
658
- label: "Import it into your library",
659
- preview: `Copy ${s.name} into your library and track it here, so other repos can use it.`,
660
- run: (w2) => w2.ops.importLocal(projectName, s.name)
661
- }
662
- ]
663
- });
664
- if (s.source === "lib" && w.days && u.uses === 0 && !s.missing && (s.added ?? 0) <= Date.now() - 30 * 24 * 60 * 60 * 1e3)
665
- issues.push({
666
- id: `unused:${s.name}`,
667
- severity: "hint",
668
- title: "Not used here in 30 days (Claude Code)",
669
- short: "Unused 30 days",
670
- decision: true,
671
- fixes: [{ ...remove, label: "Remove it from this repo", preview: `Delete ${s.dir}/${s.name} in ${projectName}. Your library keeps it.` }]
672
- });
673
- return issues;
674
- }
675
425
  function agentSkillFix(w) {
676
426
  return {
677
427
  label: w.agentSkill === "outdated" ? "Update the skilllib skill" : "Install the skilllib skill for your agents",
@@ -686,35 +436,11 @@ function removeAgentSkillFix() {
686
436
  run: (w) => w.ops.removeAgentSkill()
687
437
  };
688
438
  }
689
- function agentSkillIssue(w) {
690
- if (w.agentSkill === "installed" || !w.agents.length) return null;
691
- const missing = w.agentSkill === "missing";
692
- return {
693
- id: "agent-skill",
694
- severity: missing ? "hint" : "warning",
695
- title: missing ? "Your agents don't know about skilllib" : "The skilllib skill for your agents is out of date",
696
- short: missing ? "Your agents don't know about skilllib" : "Out of date",
697
- decision: missing,
698
- fixes: [agentSkillFix(w)]
699
- };
700
- }
701
439
  function runAll(w, fixes) {
702
- const held = [];
703
- const bad = [];
704
- for (const fix of fixes) {
705
- let r;
706
- try {
707
- r = fix.run(w);
708
- } catch (e) {
709
- r = failed(e.message);
710
- }
711
- if (isFailure(r)) bad.push(said(r));
712
- else if (followUp(r)) held.push(said(r));
713
- }
714
- const ok = fixes.length - held.length - bad.length;
440
+ const bad = fixes.map((fix) => fix.run(w)).filter(isFailure).map(said);
441
+ const ok = fixes.length - bad.length;
715
442
  const message = [
716
443
  `Applied ${ok}${ok === fixes.length ? "" : ` of ${fixes.length}`} fix${fixes.length === 1 ? "" : "es"}${ok ? " (anything removed is in Settings \u203A Backups)" : ""}`,
717
- held.length ? `${held.join("; ")} (fix ${held.length === 1 ? "it" : "them"} on ${held.length === 1 ? "its" : "their"} own to go ahead)` : "",
718
444
  bad.length ? `couldn't: ${bad.join("; ")}` : ""
719
445
  ].filter(Boolean).join(" \xB7 ");
720
446
  return bad.length ? failed(message) : message;
@@ -725,13 +451,9 @@ function worst(issues) {
725
451
  function tally(issues) {
726
452
  return {
727
453
  problem: issues.filter((i) => i.severity === "problem").length,
728
- warning: issues.filter((i) => i.severity === "warning").length,
729
- hint: issues.filter((i) => i.severity === "hint").length
454
+ suggestion: issues.filter((i) => i.severity === "suggestion").length
730
455
  };
731
456
  }
732
- function projectIssues(w, projectName) {
733
- return usable(w, projectName).filter((u) => u.local).flatMap((u) => issuesOf(w, projectName, u).map((issue) => ({ skill: u, issue })));
734
- }
735
457
  function deleteLibraryFix(w, names) {
736
458
  const tracked = w.projects.flatMap((p) => p.skills.filter((s) => names.includes(s.name) && s.source === "lib").map((s) => ({ repo: p.name, edited: s.edited })));
737
459
  const repos = [...new Set(tracked.map((t) => t.repo))];
@@ -742,7 +464,7 @@ function deleteLibraryFix(w, names) {
742
464
  label: `Delete ${names.length === 1 ? "" : `all ${names.length} `}from your library${repos.length ? ` and ${repos.length} repo${repos.length === 1 ? "" : "s"}\u2026` : ""}`,
743
465
  preview: [
744
466
  `Delete ${what} and ${names.length === 1 ? "its" : "their"} versions from your library.`,
745
- repos.length ? `Removes ${names.length === 1 ? "it" : "them"} from ${repos.length === 1 ? "this repo" : `these ${repos.length} repos`}: ${repos.join(", ")}.` : "No repo uses it.",
467
+ repos.length ? `Removes ${names.length === 1 ? "it" : "them"} from ${repos.length === 1 ? repos[0] : `these ${repos.length} repos: ${repos.join(", ")}`}.` : "No repo uses it.",
746
468
  edited.length ? `${edited.join(", ")} ${edited.length === 1 ? "has" : "have"} local edits: backed up too.` : "",
747
469
  "Every copy goes to Settings \u203A Backups.",
748
470
  others.length ? `Untracked or the team's own copies in ${others.join(", ")} stay.` : ""
@@ -781,23 +503,18 @@ function replacePluginFix(p) {
781
503
  pickRepos: (w, repos) => w.ops.replacePlugin(p, repos)
782
504
  };
783
505
  }
784
- function pluginFixFor(w, id) {
785
- const ofId = w.plugins.filter((p2) => p2.agent === "claude-code" && p2.id === id);
786
- const p = ofId.find((x) => !x.repo) ?? ofId[0];
787
- if (p) return replacePluginFix(p);
788
- return { label: "Turn the plugin off with /plugin in Claude Code", preview: `skilllib can't tell where ${id} is installed.`, run: () => `Turn ${id} off with /plugin in Claude Code; skilllib picks it up next time` };
789
- }
790
506
  function pluginSwitchFix(p, on) {
791
507
  const where = p.repo ? ` in ${p.repo} (${p.scope} install)` : p.scope === "claude.ai" ? " on this machine" : " in every repo";
792
508
  return {
793
- label: on ? "Turn it on" : "Turn it off",
794
- preview: `Turns ${p.id} ${on ? "on" : "off"}${where}${p.parts.length ? `, with its ${p.parts.join(", ")}` : ""}. ${p.scope === "claude.ai" ? `Sets "${p.id}": ${on} in ~/.claude/settings.json.` : `Runs claude plugin ${on ? "enable" : "disable"} ${p.id} --scope ${p.scope}${p.repo ? ` in ${p.repo}` : ""}.`} Claude Code picks it up in its next session.`,
509
+ // doctor's wording, so the details list it once when an issue offers it too.
510
+ label: `Turn the plugin ${p.id} ${on ? "on" : "off"}`,
511
+ preview: `Turns ${p.id} ${on ? "on" : "off"}${where}${p.parts.length ? `, with its ${p.parts.join(", ")}` : ""}. ${p.scope === "claude.ai" || p.settingsOnly ? `Sets "${p.id}": ${on} in Claude Code's settings.` : `Runs claude plugin ${on ? "enable" : "disable"} ${p.id} --scope ${p.scope}${p.repo ? ` in ${p.repo}` : ""} (without the claude command, sets it in Claude Code's settings).`} Claude Code picks it up in its next session.`,
795
512
  run: (w) => w.ops.setPlugin(p, on)
796
513
  };
797
514
  }
798
515
  function uninstallPluginFix(p) {
799
516
  return {
800
- label: "Uninstall",
517
+ label: `Uninstall the plugin ${p.id}`,
801
518
  preview: `Runs claude plugin uninstall ${p.id} --keep-data --scope ${p.scope}${p.repo ? ` in ${p.repo}` : ""}: its ${[plural(p.skills.length, "skill"), ...p.parts].join(", ")} go, its saved data stays. Settings \u203A Backups reinstalls it.`,
802
519
  run: (w) => w.ops.uninstallPlugin(p)
803
520
  };
@@ -809,58 +526,6 @@ function updatePluginFix(p) {
809
526
  run: (w) => w.ops.updatePlugin(p)
810
527
  };
811
528
  }
812
- function pluginIssues(w, p) {
813
- const library = new Set(w.library.map((l) => l.name));
814
- const global = new Set(w.machine.filter((m) => m.source === "global" && !m.broken).map((m) => m.name));
815
- if (p.agent === "cursor") {
816
- const dupes = p.skills.filter((s) => library.has(s.name) || global.has(s.name));
817
- return dupes.length ? [
818
- {
819
- id: `plugin-dupes:${p.key}`,
820
- severity: "warning",
821
- title: `Repeats your ${dupes.map((s) => s.name).join(", ")}: Cursor lists both while it's on`,
822
- short: `Repeats ${dupes.length} of yours`,
823
- decision: true,
824
- fixes: [{ label: "Turn it off in Cursor (Settings \u203A Plugins)", preview: "skilllib can't read or change Cursor's plugins.", run: () => `Turn ${p.id} off in Cursor; skilllib can't tell whether it's on` }]
825
- }
826
- ] : [];
827
- }
828
- const issues = [];
829
- if (p.on) {
830
- const repo = p.repo ? w.projects.find((x) => x.name === p.repo) : void 0;
831
- const here = repo ? new Set(repo.skills.map((s) => s.name)) : void 0;
832
- const yours = (n) => global.has(n) || (p.repo ? !!here?.has(n) : library.has(n));
833
- const dupes = p.skills.filter((s) => yours(s.name));
834
- if (dupes.length)
835
- issues.push({
836
- id: `plugin-dupes:${p.key}`,
837
- severity: "warning",
838
- title: `Repeats your ${dupes.map((s) => s.name).join(", ")}`,
839
- short: `Repeats ${dupes.length} of yours`,
840
- decision: true,
841
- fixes: [replacePluginFix(p), pluginSwitchFix(p, false)]
842
- });
843
- if (w.days && p.skills.length && pluginUses(w, p) === 0)
844
- issues.push({
845
- id: `plugin-unused:${p.key}`,
846
- severity: "hint",
847
- title: `Its skills weren't used in 30 days (Claude Code)${p.parts.length ? `; it also brings ${p.parts.join(", ")}` : ""}`,
848
- short: "Unused 30 days",
849
- decision: true,
850
- fixes: [pluginSwitchFix(p, false), ...p.scope === "claude.ai" || p.settingsOnly ? [] : [uninstallPluginFix(p)]]
851
- });
852
- }
853
- if (p.update)
854
- issues.push({
855
- id: `plugin-update:${p.key}`,
856
- severity: "hint",
857
- title: p.update === "newer" ? "Its marketplace has a newer commit" : `Update available: ${p.version ?? "?"} \u2192 ${p.update}`,
858
- short: p.update === "newer" ? "Update available" : `Update to ${p.update}`,
859
- decision: true,
860
- fixes: [updatePluginFix(p)]
861
- });
862
- return issues;
863
- }
864
529
  function originGroup(origin) {
865
530
  if (!origin) return null;
866
531
  const plugin = origin.match(/^plugin: (.+)$/);
@@ -923,8 +588,8 @@ function addRows(w, target, q) {
923
588
  const taken = new Set(target ? project(w, target).skills.map((s) => s.name) : []);
924
589
  const loaded = new Set(target ? usable(w, target).map((u) => u.name) : []);
925
590
  const hit = (name, description) => !q || matches(name, q) || q.length >= 3 && description.toLowerCase().includes(q);
926
- const groups = assignGroups(w.library.map((l) => groupCandidates(w, l)));
927
- const shown = w.library.map((l, i) => ({ l, g: groups[i] })).filter(({ l }) => !taken.has(l.name) && hit(l.name, w.descriptions[l.name] ?? ""));
591
+ const groups = target ? assignGroups(w.library.map((l) => groupCandidates(w, l))) : [];
592
+ const shown = target ? w.library.map((l, i) => ({ l, g: groups[i] })).filter(({ l }) => !taken.has(l.name) && hit(l.name, w.descriptions[l.name] ?? "")) : [];
928
593
  const skillRow = (l, inGroup) => ({
929
594
  key: `lib:${l.name}`,
930
595
  name: l.name,
@@ -936,7 +601,7 @@ function addRows(w, target, q) {
936
601
  });
937
602
  const library = [];
938
603
  const done = /* @__PURE__ */ new Set();
939
- for (const { l, g } of target ? shown : []) {
604
+ for (const { l, g } of shown) {
940
605
  if (done.has(l.name)) continue;
941
606
  const members = g ? shown.filter((x) => x.g?.key === g.key).map((x) => x.l) : [l];
942
607
  members.forEach((m) => done.add(m.name));
@@ -969,7 +634,7 @@ function addRows(w, target, q) {
969
634
  }
970
635
 
971
636
  // src/tui/prompts.ts
972
- var agentNames2 = (agents) => agents.map((a) => harness(a).name).join(", ") || "none of my agents";
637
+ var agentNames = (agents) => agents.map((a) => harness(a).name).join(", ") || "none of my agents";
973
638
  var localSource = (s) => s.source === "lib" ? "Your skills (skilllib library), installed in the repo" : s.source === "repo" ? "a repo's own skill (committed by the team)" : "a copy only on this machine (not committed)";
974
639
  var localInstalled = (s) => s.origin ?? (s.source === "lib" ? "added from my library with skilllib" : s.source === "repo" ? "committed in the repo" : "copied into the repo by hand (not committed)");
975
640
  function reviewPrompt(w, names, scope, repo) {
@@ -977,7 +642,7 @@ function reviewPrompt(w, names, scope, repo) {
977
642
  const copies = w.machine.filter((m2) => m2.name === name);
978
643
  const here = repo ? w.projects.find((p) => p.name === repo)?.skills.find((s) => s.name === name) : void 0;
979
644
  if (here) {
980
- const lib3 = w.library.find((l) => l.name === name);
645
+ const lib2 = w.library.find((l) => l.name === name);
981
646
  return {
982
647
  name,
983
648
  description: w.descriptions[name] ?? "",
@@ -985,34 +650,34 @@ function reviewPrompt(w, names, scope, repo) {
985
650
  installedHow: localInstalled(here),
986
651
  path: here.path,
987
652
  links: [],
988
- loadedBy: `${agentNames2(here.agents)} \u2014 in ${repo}`,
653
+ loadedBy: `${agentNames(here.agents)} \u2014 in ${repo}`,
989
654
  vendor: null,
990
655
  usesTotal: w.days ? totalUses(w, name) : null,
991
656
  usesByProject: Object.entries(w.usage).flatMap(([p, u]) => u[name] ? [[p, u[name]]] : []),
992
657
  installedIn: w.projects.filter((p) => p.skills.some((s) => s.name === name)).map((p) => p.name),
993
658
  otherCopies: copies.map((c) => `${c.source} ${c.where}`),
994
- inYourSkills: lib3 ? `yes, v${lib3.latest}` : null,
659
+ inYourSkills: lib2 ? `yes, v${lib2.latest}` : null,
995
660
  keptGlobal: void 0
996
661
  };
997
662
  }
998
663
  const m = copies.find((c) => c.source === "global") ?? copies[0];
999
- const lib2 = w.library.find((l) => l.name === name);
664
+ const lib = w.library.find((l) => l.name === name);
1000
665
  const repos = w.projects.filter((p) => p.skills.some((s) => s.name === name));
1001
666
  const local = repos[0]?.skills.find((s) => s.name === name);
1002
667
  return {
1003
668
  name,
1004
669
  description: w.descriptions[name] ?? "",
1005
- source: m ? m.source === "global" ? "my global skills" : `vendor: ${m.source} (${m.where})` : lib2 ? "Your skills (skilllib library)" : local ? localSource(local) : "a repo's own skill",
1006
- installedHow: m?.origin ?? (m ? m.where : lib2 ? "created or imported into skilllib" : local ? localInstalled(local) : "committed in the repo"),
1007
- path: m?.path ?? (lib2 ? w.ops.libraryFile(name).replace(/\/SKILL\.md$/, "") : local?.path ?? ""),
670
+ source: m ? m.source === "global" ? "my global skills" : `vendor: ${m.source} (${m.where})` : lib ? "Your skills (skilllib library)" : local ? localSource(local) : "a repo's own skill",
671
+ installedHow: m?.origin ?? (m ? m.where : lib ? "created or imported into skilllib" : local ? localInstalled(local) : "committed in the repo"),
672
+ path: m?.path ?? (lib ? w.ops.libraryFile(name).replace(/\/SKILL\.md$/, "") : local?.path ?? ""),
1008
673
  links: m?.links ?? [],
1009
- loadedBy: m ? `${agentNames2(m.agents)} \u2014 in every repo` : repos.map((p) => `${agentNames2(p.skills.find((s) => s.name === name).agents)} \u2014 in ${p.name}`).join("; ") || "none of my agents",
674
+ loadedBy: m ? `${agentNames(m.agents)} \u2014 in every repo` : repos.map((p) => `${agentNames(p.skills.find((s) => s.name === name).agents)} \u2014 in ${p.name}`).join("; ") || "none of my agents",
1010
675
  vendor: m && m.source !== "global" ? "turn it off at its source (/plugin in Claude Code, claude.ai settings, or Cursor)" : null,
1011
676
  usesTotal: w.days ? totalUses(w, name) : null,
1012
677
  usesByProject: Object.entries(w.usage).flatMap(([p, u]) => u[name] ? [[p, u[name]]] : []),
1013
678
  installedIn: repos.map((p) => p.name),
1014
679
  otherCopies: copies.filter((c) => c !== m).map((c) => `${c.source} ${c.where}`),
1015
- inYourSkills: lib2 ? `yes, v${lib2.latest}` : null,
680
+ inYourSkills: lib ? `yes, v${lib.latest}` : null,
1016
681
  keptGlobal: m?.kept
1017
682
  };
1018
683
  });
@@ -1039,7 +704,6 @@ Keep it under 200 lines. ${p ? "skilllib will offer to import it into my library
1039
704
 
1040
705
  // src/tui/App.tsx
1041
706
  import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1042
- var tildify3 = (p) => tildify(p);
1043
707
  var PLACES = [
1044
708
  ["projects", "Projects"],
1045
709
  ["library", "Your skills"],
@@ -1058,13 +722,12 @@ var CHIPS = [
1058
722
  ];
1059
723
  var SEV = {
1060
724
  problem: { icon: "\u2715", color: color.red },
1061
- warning: { icon: "\u26A0", color: color.yellow },
1062
- hint: { icon: "\xB7", color: color.blue }
725
+ suggestion: { icon: "\u26A0", color: color.yellow }
1063
726
  };
1064
727
  var SIDEBAR_AT = 80;
1065
728
  var SIDEBAR_W = 28;
1066
729
  var bySeverity = (a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity];
1067
- function tag(u, w) {
730
+ function tag(u) {
1068
731
  const s = u.local;
1069
732
  if (s?.source === "lib") {
1070
733
  return [`lib v${s.version}`, color.accent];
@@ -1129,10 +792,6 @@ function byName(copies) {
1129
792
  for (const c of copies) groups.set(c.name, [...groups.get(c.name) ?? [], c]);
1130
793
  return [...groups.values()].map((g) => g.sort((a, b) => COPY_RANK[a.source] - COPY_RANK[b.source]));
1131
794
  }
1132
- function mergeIssues(lists) {
1133
- const seen = /* @__PURE__ */ new Set();
1134
- return lists.flat().filter((i) => !seen.has(i.id) && seen.add(i.id));
1135
- }
1136
795
  var BLOCKS = " \u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
1137
796
  function barChart(values, width, height = 3) {
1138
797
  const max = Math.max(1, ...values);
@@ -1171,7 +830,7 @@ function usageLines(w, name, width, repo) {
1171
830
  function skillItem(w, copies, issues, nameW, actions, repo) {
1172
831
  const u = copies[0];
1173
832
  const top = worst(issues);
1174
- const [t, tc] = tag(u, w);
833
+ const [t, tc] = tag(u);
1175
834
  return {
1176
835
  key: u.name,
1177
836
  name: u.name,
@@ -1202,17 +861,44 @@ function skillItem(w, copies, issues, nameW, actions, repo) {
1202
861
  const s = c.local;
1203
862
  const path = s ? `${s.dir}/${c.name}` : c.source === "global" || c.source === "skilllib" ? `${c.where}/${c.name}` : c.where;
1204
863
  const extra = s ? s.missing ? "missing" : s.git ? GIT_LABEL[s.git] : "" : "";
1205
- const [ct, cc] = c === u ? ["", void 0] : tag(c, w);
864
+ const [ct, cc] = c === u ? ["", void 0] : tag(c);
1206
865
  b.line([" ", void 0], [path + " ", color.text], [ct ? ct + " " : "", cc], [extra, color.faint]);
866
+ for (const dir of s?.others ?? []) b.line([" ", void 0], [`${dir}/${c.name} `, color.text], ["another copy", color.faint]);
1207
867
  }
1208
868
  return b.d;
1209
869
  }
1210
870
  };
1211
871
  }
1212
872
  function projectSkillItems(w, p, nameW, ui) {
1213
- return byName(usable(w, p)).map(
1214
- (copies) => skillItem(w, copies, mergeIssues(copies.filter((c) => c.local).map((c) => issuesOf(w, p, c))), nameW, skillActions(w, p, copies[0], ui), p)
1215
- );
873
+ return [
874
+ ...byName(usable(w, p)).map(
875
+ (copies) => skillItem(w, copies, copies.some((c) => c.local) ? issuesAt(w, p, copies[0].name) : [], nameW, skillActions(w, p, copies[0], ui), p)
876
+ ),
877
+ // A link to nothing loads nothing, but its issue still needs a row (and its fix, Fix all).
878
+ ...unlisted(w, p).map((name) => {
879
+ const issues = issuesAt(w, p, name);
880
+ const top = worst(issues);
881
+ return {
882
+ key: name,
883
+ name,
884
+ group: "local",
885
+ issues,
886
+ uses: 0,
887
+ cells: [
888
+ { text: SEV[top.severity].icon, width: 2, color: SEV[top.severity].color },
889
+ { text: clip(name, nameW), width: nameW },
890
+ { text: "\u2013", width: 14, color: color.faint },
891
+ { text: usesText(0), width: 6, color: color.muted },
892
+ { text: " " + top.short, grow: true, color: SEV[top.severity].color }
893
+ ],
894
+ detail: () => {
895
+ const b = builder(name);
896
+ b.issues(issues);
897
+ return b.d;
898
+ }
899
+ };
900
+ })
901
+ ];
1216
902
  }
1217
903
  var reviewOption = (w, name, ui, repo) => ({
1218
904
  label: "Review prompt",
@@ -1233,7 +919,7 @@ function skillActions(w, p, u, ui) {
1233
919
  const remove = {
1234
920
  // Its folder gone: all that's left to remove is the skilllib.json entry (the label Health offers too).
1235
921
  label: s?.missing ? "Remove it from skilllib.json" : "Remove from repo",
1236
- preview: s?.missing ? `${p}'s skilllib.json stops pinning ${u.name}. There's no folder to delete.` : s?.source === "lib" ? `Delete ${s.dir}/${u.name} in ${p}.${inLibrary ? " Your library keeps it." : ""}` : `Move ${s?.dir}/${u.name} in ${p} to Settings \u203A Backups.`,
922
+ preview: (s?.missing ? `${p}'s skilllib.json stops pinning ${u.name}. There's no folder to delete.` : s?.source === "lib" ? `Delete ${s.dir}/${u.name} in ${p}.${inLibrary ? " Your library keeps it." : ""}` : `Move ${s?.dir}/${u.name} in ${p} to Settings \u203A Backups.`) + (s?.others ? ` The copy in ${s.others.join(", ")} stays.` : ""),
1237
923
  run: (w2) => w2.ops.remove(p, u.name)
1238
924
  };
1239
925
  if (s?.source === "lib")
@@ -1255,7 +941,6 @@ function skillActions(w, p, u, ui) {
1255
941
  ];
1256
942
  if (s)
1257
943
  return [
1258
- inLibrary ? fixOption({ label: "Track it", preview: `Record ${u.name} in skilllib.json so library updates reach it.`, run: (w2) => w2.ops.track(p, u.name) }) : fixOption({ label: "Import it into your library", preview: `Copy ${u.name} into your library and track it here.`, run: (w2) => w2.ops.importLocal(p, u.name) }),
1259
944
  { label: "Edit SKILL.md", action: () => ui.edit(`${s.path}/SKILL.md`) },
1260
945
  fixOption(remove),
1261
946
  reviewOption(w, u.name, ui, p)
@@ -1264,13 +949,10 @@ function skillActions(w, p, u, ui) {
1264
949
  }
1265
950
  function overview(w, p) {
1266
951
  const project2 = project(w, p);
1267
- const info = w.ops.repoInfo(p);
1268
952
  const manifest = project2.manifest === "none" ? ["no skilllib.json", color.faint] : project2.manifest === "no git" ? ["skilllib.json (not a git repo)", color.muted] : [`skilllib.json ${GIT_LABEL[project2.manifest]}`, project2.manifest === "committed" ? color.text : color.yellow];
1269
953
  const plugins = [...new Set(machineIn(w, project2).filter((m) => m.source === "plugin").map((m) => m.where.split("@")[0]))];
1270
954
  return [
1271
- ["Folder", [[tildify3(project2.path), color.text]]],
1272
- ["Remote", [[info.remote ?? "none", info.remote ? color.blue : color.faint]]],
1273
- ["Branch", [[info.branch ?? "\u2013", color.text], [info.dirty ? ` ${info.dirty} uncommitted` : "", color.yellow]]],
955
+ ["Folder", [[tildify(project2.path), color.text]]],
1274
956
  ["Manifest", [manifest]],
1275
957
  ["Plugins", [[plugins.length ? plugins.join(", ") : "none", plugins.length ? color.magenta : color.faint]]]
1276
958
  ];
@@ -1292,7 +974,7 @@ function InfoGrid({ rows, columns, width }) {
1292
974
  }
1293
975
  function projectItems(w) {
1294
976
  return w.projects.map((p) => {
1295
- const issues = projectIssues(w, p.name).map((x) => x.issue);
977
+ const issues = repoIssues(w, p.name);
1296
978
  const t = tally(issues);
1297
979
  return {
1298
980
  key: p.name,
@@ -1304,15 +986,14 @@ function projectItems(w) {
1304
986
  { text: p.name, width: 18 },
1305
987
  { text: p.skills.length ? `${p.skills.length} skill${p.skills.length === 1 ? "" : "s"}` : "\u2013", width: 10, color: color.muted },
1306
988
  { text: t.problem ? `\u2715 ${t.problem}` : "", width: 5, color: color.red },
1307
- { text: t.warning ? `\u26A0 ${t.warning}` : "", width: 5, color: color.yellow },
1308
- { text: t.hint ? `\xB7 ${t.hint}` : "", grow: true, color: color.blue }
989
+ { text: t.suggestion ? `\u26A0 ${t.suggestion}` : "", grow: true, color: color.yellow }
1309
990
  ]
1310
991
  };
1311
992
  });
1312
993
  }
1313
994
  function globalItems(w, nameW, ui) {
1314
995
  const copies = w.machine.map((m) => ({ name: m.name, source: m.source, where: m.where, agents: m.agents, uses: totalUses(w, m.name), machine: m }));
1315
- return byName(copies).map((g) => skillItem(w, g, mergeIssues(g.map((c) => machineIssues(w, c.machine))), nameW, globalActions(w, g[0], ui), null));
996
+ return byName(copies).map((g) => skillItem(w, g, issuesAt(w, null, g[0].name), nameW, globalActions(w, g[0], ui), null));
1316
997
  }
1317
998
  var AT_SOURCE = {
1318
999
  global: null,
@@ -1340,7 +1021,8 @@ function libraryItems(w, nameW, ui) {
1340
1021
  const uses = totalUses(w, l.name);
1341
1022
  const addFix = { label: "Add to repos\u2026", preview: `Install ${l.name} v${l.latest} into the repos you pick.`, run: () => "", candidates: (w2) => w2.projects.filter((p) => !p.skills.some((s) => s.name === l.name)).map((p) => p.name), pickRepos: (w2, rs) => w2.ops.addTo(rs, [l.name]) };
1342
1023
  const delFix = deleteLibraryFix(w, [l.name]);
1343
- const issues = repos.length ? [] : [{ id: `nowhere:${l.name}`, severity: "hint", title: "Used in no repo", short: "Used in no repo", decision: true, fixes: [addFix, delFix] }];
1024
+ const issues = libraryIssues(w, l.name);
1025
+ const sev = issues[0] && SEV[issues[0].severity];
1344
1026
  const behind = repos.filter((p) => p.skills.some((s) => s.name === l.name && s.source === "lib" && s.version < l.latest)).length;
1345
1027
  return {
1346
1028
  key: l.name,
@@ -1350,12 +1032,12 @@ function libraryItems(w, nameW, ui) {
1350
1032
  cands: groupCandidates(w, l),
1351
1033
  lib: l,
1352
1034
  cells: [
1353
- { text: issues[0] ? SEV.hint.icon : " ", width: 2, color: color.blue },
1035
+ { text: sev ? sev.icon : " ", width: 2, color: sev?.color },
1354
1036
  { text: clip(l.name, nameW), width: nameW },
1355
1037
  { text: `v${l.latest}`, width: 5, color: color.accent },
1356
- { text: `${repos.length} repo${repos.length === 1 ? "" : "s"}${behind ? ` (${behind} behind)` : ""}`, width: 18, color: behind ? color.yellow : color.muted },
1038
+ { text: clip(`${repos.length} repo${repos.length === 1 ? "" : "s"}${behind ? `, ${behind} behind` : ""}`, 16), width: 16, color: behind ? color.yellow : color.muted },
1357
1039
  { text: usesText(uses), width: 6, color: color.muted },
1358
- { text: " " + (issues[0]?.short ?? w.descriptions[l.name] ?? ""), grow: true, color: issues[0] ? color.blue : color.faint }
1040
+ { text: " " + (issues[0]?.short ?? w.descriptions[l.name] ?? ""), grow: true, color: sev ? sev.color : color.faint }
1359
1041
  ],
1360
1042
  detail: (width) => {
1361
1043
  const b = builder(l.name);
@@ -1390,43 +1072,37 @@ function libraryItems(w, nameW, ui) {
1390
1072
  };
1391
1073
  });
1392
1074
  }
1393
- function healthItems(w, nameW, ui) {
1394
- const flagged = (where, items) => items.filter((i) => i.issues.length).map((i) => ({ ...i, key: `${where}:${i.key}`, cells: [i.cells[0], i.cells[1], { text: clip(where, 16), width: 16, color: color.muted }, i.cells[4]] }));
1395
- return [...w.projects.flatMap((p) => flagged(p.name, projectSkillItems(w, p.name, nameW, ui))), ...flagged("Global", globalItems(w, nameW, ui)), ...agentSkillItems(w, nameW)];
1396
- }
1397
- function agentSkillItems(w, nameW) {
1398
- const issue = agentSkillIssue(w);
1399
- if (!issue || w.machine.some((m) => m.source === "skilllib")) return [];
1400
- const sev = SEV[issue.severity];
1401
- return [
1402
- {
1403
- key: "agent-skill",
1404
- name: "skilllib",
1075
+ function healthItems(w) {
1076
+ return w.issues.map((found) => {
1077
+ const issue = issueOf(found);
1078
+ const sev = SEV[issue.severity];
1079
+ return {
1080
+ key: issue.id,
1081
+ name: issue.title,
1405
1082
  issues: [issue],
1406
1083
  uses: 0,
1407
1084
  cells: [
1408
1085
  { text: sev.icon, width: 2, color: sev.color },
1409
- { text: clip("skilllib", nameW), width: nameW },
1410
- { text: clip("Your agents", 16), width: 16, color: color.muted },
1411
- { text: " " + issue.short, grow: true, color: sev.color }
1086
+ { text: issue.title, grow: true }
1412
1087
  ],
1413
1088
  detail: (width) => {
1414
- const b = builder("skilllib");
1415
- for (const l of wrap("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.", width - 4, 3)) b.line([l, color.muted]);
1089
+ const b = builder(issue.short);
1090
+ for (const l of wrap(found.detail, width - 4, Infinity)) b.line([l, color.muted]);
1416
1091
  b.line();
1417
1092
  b.issues([issue]);
1418
1093
  return b.d;
1419
1094
  }
1420
- }
1421
- ];
1095
+ };
1096
+ });
1422
1097
  }
1423
- var pluginW = (nameW) => Math.min(nameW, 17);
1098
+ var pluginCols = (columns, nameW) => columns >= 76 ? { wide: true, name: Math.min(nameW, 17), scope: 19 } : { wide: false, name: 15, scope: 12 };
1424
1099
  var STATE = { on: ["on", color.green], off: ["off", color.faint], unknown: ["?", color.faint] };
1425
- function pluginItems(w, skillW, ui, issuesOf2) {
1426
- const nameW = pluginW(skillW);
1100
+ function pluginItems(w, columns, skillW, ui, issuesOf) {
1101
+ const { wide, name: nameW, scope: scopeW } = pluginCols(columns, skillW);
1427
1102
  return w.plugins.map((p) => {
1428
- const issues = issuesOf2.get(p.key) ?? [];
1103
+ const issues = issuesOf.get(p.key) ?? [];
1429
1104
  const top = worst(issues);
1105
+ const repeats = new Set(issues.flatMap((i) => i.skills)).size;
1430
1106
  const cursor = p.agent === "cursor";
1431
1107
  const uses = cursor ? 0 : pluginUses(w, p);
1432
1108
  const [state, stateColor] = STATE[p.on === null ? "unknown" : p.on ? "on" : "off"];
@@ -1448,11 +1124,12 @@ function pluginItems(w, skillW, ui, issuesOf2) {
1448
1124
  { text: top ? SEV[top.severity].icon : " ", width: 2, color: top ? SEV[top.severity].color : void 0 },
1449
1125
  { text: clip(name, nameW), width: nameW, color: p.on === false ? color.muted : void 0 },
1450
1126
  { text: harness(p.agent).icon, width: 3, color: harness(p.agent).color },
1451
- { text: clip(cursor ? "\u2013" : pluginScope(p), 19), width: 19, color: p.repo ? color.blue : color.muted },
1452
- { text: clip(`${n} skill${n === 1 ? "" : "s"}${p.parts.length ? ` +${p.parts.length}` : ""}`, 13), width: 13, color: color.muted },
1127
+ { text: clip(cursor ? "\u2013" : pluginScope(p), scopeW), width: scopeW, color: p.repo ? color.blue : color.muted },
1128
+ ...wide ? [{ text: clip(`${n} skill${n === 1 ? "" : "s"}${p.parts.length ? ` +${p.parts.length}` : ""}`, 13), width: 13, color: color.muted }] : [],
1453
1129
  { text: state.padEnd(4), width: 4, color: stateColor },
1454
- { text: cursor ? " \u2013 " : usesText(uses), width: 6, color: color.muted },
1455
- { text: " " + (top?.short ?? ""), grow: true, color: top ? SEV[top.severity].color : color.faint }
1130
+ ...wide ? [{ text: cursor ? " \u2013 " : usesText(uses), width: 6, color: color.muted }] : [],
1131
+ // Your skills it repeats, across its issues; else its worst issue.
1132
+ { text: " " + (repeats ? `Repeats ${repeats} of yours` : top?.short ?? ""), grow: true, color: top ? SEV[top.severity].color : color.faint }
1456
1133
  ],
1457
1134
  detail: (width) => {
1458
1135
  const b = builder(p.id);
@@ -1474,7 +1151,7 @@ function pluginItems(w, skillW, ui, issuesOf2) {
1474
1151
  b.line(label("Also brings"), [p.parts.join(", ") || "nothing else", p.parts.length ? color.text : color.faint]);
1475
1152
  const installed = cursor ? "in Cursor (whether it's on is only in Cursor)" : p.scope === "claude.ai" ? "synced from your claude.ai account" : p.repo ? `${p.scope} scope, in ${p.repo} only` : `user scope, in every repo`;
1476
1153
  b.line(label("Installed"), [installed, color.text], [p.version ? ` v${p.version.replace(/^v/, "")}` : "", color.muted], [p.id.includes("@") && p.scope !== "claude.ai" ? ` from ${p.id.split("@")[1]}` : "", color.faint]);
1477
- b.line(label("Folder"), [tildify3(p.path), color.faint]);
1154
+ b.line(label("Folder"), [tildify(p.path), color.faint]);
1478
1155
  return b.d;
1479
1156
  }
1480
1157
  };
@@ -1492,7 +1169,9 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1492
1169
  const columns = sidebar ? termCols - SIDEBAR_W : termCols;
1493
1170
  const [base, setBase] = useState(initial);
1494
1171
  const [used, setUsed] = useState();
1495
- const world = used ? { ...base, ...used } : base;
1172
+ const withUsage = used ? { ...base, ...used } : base;
1173
+ const usageIssues2 = useMemo(() => withUsage.days ? base.ops.usageIssues(withUsage) : [], [base, used]);
1174
+ const world = { ...withUsage, issues: [...base.issues, ...usageIssues2] };
1496
1175
  useEffect(() => {
1497
1176
  let live = true;
1498
1177
  void loadUsage2?.(base).then((u) => live && setUsed(u));
@@ -1514,8 +1193,9 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1514
1193
  const [modal, setModal] = useState(null);
1515
1194
  const [toast, setToast] = useState("");
1516
1195
  const order = useRef({ key: "", keys: [] });
1517
- const split = termRows >= 30;
1196
+ const split = termRows >= 30 && place !== "settings";
1518
1197
  const nameW = Math.min(22, Math.max(17, Math.floor(columns * 0.22)));
1198
+ const libNameW = columns < 90 ? nameW - 4 : nameW;
1519
1199
  const dashboard = place === "projects" && open !== null;
1520
1200
  const chips = dashboard ? CHIPS : place === "global" ? CHIPS.filter(([c]) => c !== "local") : place === "settings" ? [] : place === "health" ? CHIPS.slice(0, 1) : CHIPS.slice(0, 2);
1521
1201
  const ui = {
@@ -1527,15 +1207,11 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1527
1207
  menu: (title, options) => setModal({ kind: "menu", title, options, cursor: 0 })
1528
1208
  };
1529
1209
  const pluginIssues2 = new Map(world.plugins.map((p) => [p.key, pluginIssues(world, p)]));
1530
- const flagged = [
1531
- ...world.projects.flatMap((p) => projectIssues(world, p.name).map((x) => ({ key: `${p.name}:${x.skill.name}`, issue: x.issue }))),
1532
- ...world.machine.flatMap((m) => machineIssues(world, m).map((issue) => ({ key: `Global:${m.name}`, issue }))),
1533
- // The skilllib skill's issue is on its Global row when it has one (see agentSkillItems).
1534
- ...[agentSkillIssue(world)].flatMap((issue) => issue ? [{ key: "Global:skilllib", issue }] : [])
1535
- ];
1536
- const healthCount = new Set(flagged.map((f) => f.key)).size;
1537
- const healthWorst = worst(flagged.map((f) => f.issue));
1538
- const all = place === "projects" ? open ? projectSkillItems(world, open, nameW, ui) : projectItems(world) : place === "global" ? globalItems(world, nameW, ui) : place === "library" ? libraryItems(world, nameW, ui) : place === "plugins" ? pluginItems(world, nameW, ui, pluginIssues2) : place === "health" ? healthItems(world, nameW, ui) : settingsItems();
1210
+ const repoIssues2 = new Map(world.projects.map((p) => [p.name, repoIssues(world, p.name)]));
1211
+ const machineIssues = [...new Set(world.machine.map((m) => m.name))].map((name) => [name, issuesAt(world, null, name)]);
1212
+ const healthCount = world.issues.length;
1213
+ const healthWorst = worst(world.issues.map(issueOf));
1214
+ const all = place === "projects" ? open ? projectSkillItems(world, open, nameW, ui) : projectItems(world) : place === "global" ? globalItems(world, nameW, ui) : place === "library" ? libraryItems(world, libNameW, ui) : place === "plugins" ? pluginItems(world, columns, nameW, ui, pluginIssues2) : place === "health" ? healthItems(world) : settingsItems();
1539
1215
  const inChip = (i, c) => c === "all" || (c === "issues" ? i.issues.length > 0 : i.group === c);
1540
1216
  let items = all.filter((i) => i.header || (!query || matches(i.name, query)) && inChip(i, chip));
1541
1217
  const viewKey = [place, open, chip, query, used ? "usage" : ""].join("|");
@@ -1556,7 +1232,9 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1556
1232
  if (!query) items = [...actionRows(), ...items];
1557
1233
  const cursorKey = `${place}|${open}`;
1558
1234
  const firstSelectable = Math.max(0, items.findIndex((i) => !i.header && !i.action));
1559
- let cursor = Math.min(cursors[cursorKey] ?? firstSelectable, items.length - 1);
1235
+ const saved = cursors[cursorKey];
1236
+ const kept = saved ? items.findIndex((i) => i.key === saved.key) : -1;
1237
+ let cursor = Math.min(kept >= 0 ? kept : saved?.at ?? firstSelectable, items.length - 1);
1560
1238
  if (items[cursor]?.header) cursor = firstSelectable;
1561
1239
  const current = items[cursor];
1562
1240
  const detail = current?.detail?.(columns);
@@ -1602,7 +1280,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1602
1280
  function fixAllList(from) {
1603
1281
  const seen = /* @__PURE__ */ new Set();
1604
1282
  return from.flatMap(
1605
- (i) => i.issues.filter((x) => !x.decision && !seen.has(`${i.key}|${x.id}`) && seen.add(`${i.key}|${x.id}`)).map((x) => ({ label: `${i.name}: ${x.title} \u2192 ${x.fixes[0].label}`, fix: x.fixes[0], on: true }))
1283
+ (i) => i.issues.filter((x) => !x.decision && !seen.has(x.id) && seen.add(x.id)).map((x) => ({ label: `${x.title} \u2192 ${x.fixes[0].label}`, fix: x.fixes[0], on: true }))
1606
1284
  );
1607
1285
  }
1608
1286
  function toggleFold(key) {
@@ -1630,7 +1308,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1630
1308
  const label = groupLabel(g, members.map((m) => m.name));
1631
1309
  const issues = members.flatMap((m) => m.issues);
1632
1310
  const top = worst(issues);
1633
- const flagged2 = members.filter((m) => m.issues.length).length;
1311
+ const flagged = members.filter((m) => m.issues.length).length;
1634
1312
  return {
1635
1313
  key: `fold:${g.key}`,
1636
1314
  name: label,
@@ -1641,13 +1319,13 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1641
1319
  cells: [
1642
1320
  { text: top ? SEV[top.severity].icon : " ", width: 2, color: top ? SEV[top.severity].color : void 0 },
1643
1321
  { text: `${isOpen ? "\u25BE" : "\u25B8"} ${label} (${members.length})`, grow: true, color: color.accent },
1644
- { text: flagged2 ? `${flagged2} to fix` : "", width: 10, color: top ? SEV[top.severity].color : color.faint }
1322
+ { text: flagged ? `${flagged} to fix` : "", width: 10, color: top ? SEV[top.severity].color : color.faint }
1645
1323
  ],
1646
1324
  detail: (width) => {
1647
1325
  const b = builder(label);
1648
1326
  for (const l of wrap(members.map((m) => m.name).join(", "), width - 4, 4)) b.line([l, color.muted]);
1649
1327
  b.line();
1650
- if (flagged2) b.line([`${flagged2} of them ${flagged2 === 1 ? "has" : "have"} issues`, top ? SEV[top.severity].color : color.text], [" open the group to fix them one by one", color.faint]);
1328
+ if (flagged) b.line([`${flagged} of them ${flagged === 1 ? "has" : "have"} issues`, top ? SEV[top.severity].color : color.text], [" open the group to fix them one by one", color.faint]);
1651
1329
  b.actions(groupActions(label, members), width);
1652
1330
  return b.d;
1653
1331
  }
@@ -1670,8 +1348,8 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1670
1348
  preticked: usedIn,
1671
1349
  pickRepos: (w, repos) => w.ops.moveGlobal(mine, repos, label)
1672
1350
  }),
1673
- ...mine.some((m) => !m.kept) ? [fixOption({ label: `Keep all ${mine.length} global on purpose`, preview: `skilllib stops warning about these ${n(mine.length)}.`, run: (w) => (mine.forEach((m) => w.ops.keepGlobal(m.name, true)), `${n(mine.length)} marked as global on purpose`) })] : [],
1674
- fixOption({ label: `Delete all ${mine.length}`, preview: `Move these ${n(mine.length)} to Settings \u203A Backups.`, run: (w) => (mine.forEach((m) => w.ops.deleteGlobal(m)), `Deleted ${n(mine.length)} (in Settings \u203A Backups)`) })
1351
+ ...mine.some((m) => !m.kept) ? [fixOption({ label: `Keep all ${mine.length} global on purpose`, preview: `Mark these ${n(mine.length)} as global on purpose: they no longer show as Not reviewed. Their other issues stay.`, run: (w) => (mine.forEach((m) => w.ops.keepGlobal(m.name, true)), `${n(mine.length)} marked as global on purpose`) })] : [],
1352
+ fixOption({ label: `Delete all ${mine.length}`, preview: `Move these ${n(mine.length)} to Settings \u203A Backups.`, run: (w) => joined(mine.map((m) => w.ops.deleteGlobal(m))) })
1675
1353
  ] : [],
1676
1354
  ...libs.length ? [
1677
1355
  fixOption({
@@ -1683,7 +1361,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1683
1361
  })
1684
1362
  ] : [],
1685
1363
  ...libs.length ? [fixOption(deleteLibraryFix(world, libs.map((l) => l.name)))] : [],
1686
- ...locals.length ? [fixOption({ label: `Remove all ${locals.length} from this repo`, preview: `Remove these ${n(locals.length)} from ${open}; your library keeps the tracked ones.`, run: (w) => (locals.forEach((s) => w.ops.remove(open, s.name)), `Removed ${n(locals.length)} from ${open}`) })] : [],
1364
+ ...locals.length ? [fixOption({ label: `Remove all ${locals.length} from this repo`, preview: `Remove these ${n(locals.length)} from ${open}; your library keeps the tracked ones.`, run: (w) => joined(locals.map((s) => w.ops.remove(open, s.name))) })] : [],
1687
1365
  ...[...new Set(machines.filter((m) => m.source === "plugin").map((m) => m.where))].map((id) => ({ label: `Open ${id.split("@")[0]} in Plugins`, action: () => openInPlugins(id) })),
1688
1366
  { label: "Review prompt", action: () => ui.copy(reviewPrompt(world, names, `Review these related skills (${label}).`), `a review prompt for ${label}`) }
1689
1367
  ];
@@ -1743,9 +1421,12 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1743
1421
  i = Math.max(0, Math.min(items.length - 1, i + delta));
1744
1422
  while (items[i]?.header && i > 0 && i < items.length - 1);
1745
1423
  if (items[i]?.header) return;
1746
- setCursors({ ...cursors, [cursorKey]: i });
1424
+ setCursorAt(i);
1747
1425
  setDetailCursor(0);
1748
1426
  }
1427
+ function setCursorAt(i) {
1428
+ setCursors({ ...cursors, [cursorKey]: { key: items[i]?.key ?? "", at: i } });
1429
+ }
1749
1430
  function goPlace(p) {
1750
1431
  setPlace(p);
1751
1432
  setFocus("list");
@@ -1753,7 +1434,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1753
1434
  setChip("all");
1754
1435
  }
1755
1436
  function edit(file, then) {
1756
- if (!existsSync2(file)) return then ? apply(then) : (setBase(reload()), setToast(`${tildify3(file)} doesn't exist`));
1437
+ if (!existsSync2(file)) return then ? apply(then) : (setBase(reload()), setToast(`${tildify(file)} doesn't exist`));
1757
1438
  const editor = process.env.VISUAL || process.env.EDITOR || (process.platform === "win32" ? "notepad" : "vi");
1758
1439
  void suspendTerminal(() => {
1759
1440
  if (process.platform === "win32") spawnSync2(`${editor} "${file}"`, { shell: true, stdio: "inherit" });
@@ -1762,7 +1443,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1762
1443
  const fresh = reload();
1763
1444
  if (then) return apply(then, fresh);
1764
1445
  setBase(fresh);
1765
- setToast(`Saved ${tildify3(file)}`);
1446
+ setToast(`Saved ${tildify(file)}`);
1766
1447
  });
1767
1448
  }
1768
1449
  function askForFolder(title) {
@@ -1810,16 +1491,16 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1810
1491
  ...world.roots.map(
1811
1492
  (r) => row(
1812
1493
  `root:${r}`,
1813
- tildify3(r),
1494
+ tildify(r),
1814
1495
  "scanned for git repos",
1815
- () => ui.menu(tildify3(r), [
1496
+ () => ui.menu(tildify(r), [
1816
1497
  { label: "Look for repos again", action: () => apply((w) => w.ops.rescan()) },
1817
- fixOption({ label: "Stop scanning this folder", preview: `Repos already found in ${tildify3(r)} stay listed until you hide them.`, run: (w) => w.ops.removeRoot(r) })
1498
+ fixOption({ label: "Stop scanning this folder", preview: `Repos already found in ${tildify(r)} stay listed until you hide them.`, run: (w) => w.ops.removeRoot(r) })
1818
1499
  ])
1819
1500
  )
1820
1501
  ),
1821
1502
  row("root:add", "+ Add a folder\u2026", "", () => askForFolder("Folder that contains your projects")),
1822
- ...world.hidden.length ? [header("#hidden", "Hidden repos"), ...world.hidden.map((h) => row(`hidden:${h}`, tildify3(h), "enter shows it again", () => apply((w) => w.ops.unhide(h))))] : [],
1503
+ ...world.hidden.length ? [header("#hidden", "Hidden repos"), ...world.hidden.map((h) => row(`hidden:${h}`, tildify(h), "enter shows it again", () => apply((w) => w.ops.unhide(h))))] : [],
1823
1504
  header("#backups", "Backups"),
1824
1505
  ...world.backups.length ? world.backups.map(
1825
1506
  (b, i) => row(`backup:${i}`, `\u21BA ${b.name}`, `${b.from} \xB7 ${b.at}`, () => apply((w) => w.ops.restoreBackup(i)))
@@ -1869,22 +1550,22 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1869
1550
  if (key.escape) return setSideQuery("");
1870
1551
  return;
1871
1552
  }
1553
+ if (!sidebar && (key.leftArrow || key.rightArrow)) {
1554
+ const i = PLACES.findIndex(([p]) => p === place) + (key.leftArrow ? -1 : 1);
1555
+ if (PLACES[i]) goPlace(PLACES[i][0]);
1556
+ return;
1557
+ }
1872
1558
  const fold = current?.fold;
1873
1559
  if (fold && !fold.open && (key.rightArrow || key.return || input === " ")) return toggleFold(fold.key);
1874
1560
  if (fold?.open && (key.leftArrow || input === " ")) return toggleFold(fold.key);
1875
1561
  if (current?.inFold && key.leftArrow) {
1876
1562
  toggleFold(current.inFold);
1877
- return setCursors({ ...cursors, [cursorKey]: items.findIndex((i) => i.fold?.key === current.inFold) });
1563
+ return setCursorAt(items.findIndex((i) => i.fold?.key === current.inFold));
1878
1564
  }
1879
1565
  if (sidebar && key.leftArrow) return setFocus("sidebar");
1880
1566
  if (sidebar && key.rightArrow && detail) return setFocus("detail"), setDetailCursor(0);
1881
1567
  if (key.upArrow) return move(-1);
1882
1568
  if (key.downArrow) return move(1);
1883
- if (!sidebar && (key.leftArrow || key.rightArrow)) {
1884
- const i = PLACES.findIndex(([p]) => p === place) + (key.leftArrow ? -1 : 1);
1885
- if (PLACES[i]) goPlace(PLACES[i][0]);
1886
- return;
1887
- }
1888
1569
  if (key.escape) {
1889
1570
  if (query) return setQuery("");
1890
1571
  if (sidebar) return setFocus("sidebar");
@@ -2013,7 +1694,8 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
2013
1694
  if (row.create && !/^[a-z0-9]/.test(m.query)) return setToast("Type the new skill's name first");
2014
1695
  setModal(null);
2015
1696
  if (!row.create) return apply(row.run);
2016
- row.run(world);
1697
+ const created = row.run(world);
1698
+ if (isFailure(created)) return setToast(`\u2717 ${said2(created)}`);
2017
1699
  return edit(world.ops.libraryFile(m.query), row.afterEdit);
2018
1700
  }
2019
1701
  if (isNameChar(input, key)) return setModal({ ...m, query: m.query + input.toLowerCase(), cursor: 0 });
@@ -2023,13 +1705,12 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
2023
1705
  const confirmLines = modal?.kind === "confirm" ? wrap(modal.fix.preview, columns - 6, 6) : [];
2024
1706
  const confirmH = modal?.kind === "confirm" ? confirmLines.length + 2 : 0;
2025
1707
  const bodyH = Math.max(8, termRows - (sidebar ? 0 : 1) - bottomH - confirmH);
2026
- const globalFlagged = world.machine.map((m) => [m.name, machineIssues(world, m)]).filter(([, issues]) => issues.length);
2027
- const globalIssues = new Set(globalFlagged.map(([name]) => name)).size;
1708
+ const globalFlagged = machineIssues.filter(([, issues]) => issues.length);
1709
+ const globalIssues = globalFlagged.length;
2028
1710
  const globalProblem = globalFlagged.some(([, issues]) => issues.some((x) => x.severity === "problem"));
2029
- const pluginsWorst = worst([...pluginIssues2.values()].flat().filter((i) => i.severity !== "hint"));
1711
+ const pluginsWorst = worst([...pluginIssues2.values()].flat());
2030
1712
  const badge = (p) => {
2031
- const found = projectIssues(world, p);
2032
- const top = worst(found.map((x) => x.issue));
1713
+ const top = worst(repoIssues2.get(p) ?? repoIssues(world, p));
2033
1714
  return { text: top ? SEV[top.severity].icon : "", width: 3, color: top ? SEV[top.severity].color : void 0 };
2034
1715
  };
2035
1716
  const sideRow = (key, label, here, go, count = []) => ({
@@ -2088,7 +1769,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
2088
1769
  }),
2089
1770
  /* @__PURE__ */ jsx2(Text2, { color: color.accent, children: query ? ` \u2315 ${query}\u258F` : "" })
2090
1771
  ] }) });
2091
- const columnTitles = dashboard || place === "global" ? titles([["", 2], ["Skill", nameW], ["Source", 14], [" Uses", 6], [" Issue", 0]]) : place === "projects" ? titles([["", 2], ["Project", 18], ["Skills", 10], ["Issues", 0]]) : place === "library" ? titles([["", 2], ["Skill", nameW], ["Ver", 5], ["Used in", 18], [" Uses", 6], [" Issue / description", 0]]) : place === "plugins" ? titles([["", 2], ["Plugin", pluginW(nameW)], ["", 3], ["Scope", 19], ["Brings", 13], ["On", 4], [" Uses", 6], [" Issue", 0]]) : place === "health" ? titles([["", 2], ["Skill", nameW], ["Where", 16], [" Issue", 0]]) : null;
1772
+ const columnTitles = dashboard || place === "global" ? titles([["", 2], ["Skill", nameW], ["Source", 14], [" Uses", 6], [" Issue", 0]]) : place === "projects" ? titles([["", 2], ["Project", 18], ["Skills", 10], ["Issues", 0]]) : place === "library" ? titles([["", 2], ["Skill", libNameW], ["Ver", 5], ["Used in", 16], [" Uses", 6], [" Issue / description", 0]]) : place === "plugins" ? ((c) => titles([["", 2], ["Plugin", c.name], ["", 3], ["Scope", c.scope], ...c.wide ? [["Brings", 13]] : [], ["On", 4], ...c.wide ? [[" Uses", 6]] : [], [" Issue", 0]]))(pluginCols(columns, nameW)) : place === "health" ? titles([["", 2], ["Issue", 0]]) : null;
2092
1773
  const showChips = chips.length > 0;
2093
1774
  const listHeader = /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
2094
1775
  showChips ? chipRow : null,
@@ -2272,7 +1953,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
2272
1953
  ] })
2273
1954
  ] }),
2274
1955
  /* @__PURE__ */ jsxs2(Box2, { height: 1, width: termCols, justifyContent: "space-between", children: [
2275
- /* @__PURE__ */ jsx2(Text2, { color: toast.startsWith("\u2713") || toast.startsWith("\u21BA") ? color.green : toast.startsWith("\u2717") ? color.red : toast.startsWith("!") ? color.yellow : color.muted, wrap: "truncate-end", children: " " + toast }),
1956
+ /* @__PURE__ */ jsx2(Text2, { color: toast.startsWith("\u2713") ? color.green : toast.startsWith("\u2717") ? color.red : toast.startsWith("!") ? color.yellow : color.muted, wrap: "truncate-end", children: " " + toast }),
2276
1957
  toast ? null : /* @__PURE__ */ jsx2(Text2, { color: color.faint, children: hints + " " })
2277
1958
  ] })
2278
1959
  ] });
@@ -2315,23 +1996,24 @@ function Help({ width, height }) {
2315
1996
  ["ctrl+r", "read everything again from disk"],
2316
1997
  ["ctrl+c", "quit"]
2317
1998
  ];
2318
- return /* @__PURE__ */ jsxs2(Panel, { title: "Keys", hint: "any key closes", focused: true, width, height, children: [
2319
- keys.map(([k, d]) => /* @__PURE__ */ jsxs2(Text2, { children: [
2320
- /* @__PURE__ */ jsx2(Text2, { color: color.accent, children: k.padEnd(12) }),
2321
- /* @__PURE__ */ jsx2(Text2, { color: color.text, children: d })
2322
- ] }, k)),
2323
- /* @__PURE__ */ jsx2(Text2, { children: " " }),
2324
- /* @__PURE__ */ jsx2(Text2, { color: color.muted, children: '\u2715 problem \xB7 \u26A0 warning \xB7 \xB7 hint \xB7 \u2605 recommended fix \xB7 "your call" = a decision, never auto-fixed' }),
2325
- /* @__PURE__ */ jsx2(Text2, { color: color.muted, children: "Source: lib = from your library \xB7 repo = committed by the team \xB7 untracked = only on this machine" }),
2326
- /* @__PURE__ */ jsx2(Text2, { color: color.muted, children: "Usage counts Claude Code sessions only: other agents don't record skill use." })
2327
- ] });
1999
+ const notes = [
2000
+ '\u2715 problem \xB7 \u26A0 suggestion \xB7 \u2605 recommended fix \xB7 "your call" = a decision, never auto-fixed',
2001
+ "Source: lib = from your library \xB7 repo = committed by the team \xB7 untracked = only on this machine",
2002
+ "Usage counts Claude Code sessions only: other agents don't record skill use."
2003
+ ];
2004
+ const lines = [
2005
+ ...keys.flatMap(([k, d]) => wrap(d, width - 16, 3).map((l, i) => [[(i ? "" : k).padEnd(12), color.accent], [l, color.text]])),
2006
+ [],
2007
+ ...notes.flatMap((n) => wrap(n, width - 4, 3).map((l) => [[l, color.muted]]))
2008
+ ];
2009
+ return /* @__PURE__ */ jsx2(Panel, { title: "Keys", hint: "any key closes", focused: true, width, height, children: lines.slice(0, height - 2).map((segs, i) => /* @__PURE__ */ jsx2(SegLine, { segs: segs.length ? segs : [[" "]] }, i)) });
2328
2010
  }
2329
2011
 
2330
2012
  // src/tui/load.ts
2331
- import { execFileSync, spawn } from "child_process";
2013
+ import { spawn } from "child_process";
2332
2014
  import { existsSync as existsSync3, lstatSync, realpathSync } from "fs";
2333
2015
  import { basename, dirname, join as join2 } from "path";
2334
- function tildify4(p) {
2016
+ function tildify2(p) {
2335
2017
  return tildify(p).replace(/\\/g, "/");
2336
2018
  }
2337
2019
  function uniqueNames(roots) {
@@ -2344,31 +2026,25 @@ function uniqueNames(roots) {
2344
2026
  })
2345
2027
  );
2346
2028
  }
2347
- function localSkill(root, s, git2) {
2029
+ function localSkill(root, s, git) {
2348
2030
  const managed = {
2349
- ok: {},
2350
- "update available": {},
2351
2031
  "edited locally": { edited: true },
2352
2032
  "edited locally, update available": { edited: true },
2353
- // sync restores it from your library, unless the library doesn't have it (a teammate's skill, another library).
2354
- "folder missing": { missing: true, ...s.latest === null && { library: "missing" } },
2355
- "not in library": { library: "missing" }
2033
+ "folder missing": { missing: true }
2356
2034
  };
2357
2035
  const other = {
2358
- "untracked copy of library skill": { source: "untracked", library: "same" },
2359
- "untracked, differs from library": { source: "untracked", library: "differs" },
2036
+ "untracked copy of library skill": { source: "untracked" },
2037
+ "untracked, differs from library": { source: "untracked" },
2360
2038
  "local only": { source: "untracked" },
2361
2039
  // Installed with `npx skills add` (the repo's skills-lock.json): not skilllib's, so not a library skill.
2362
2040
  "from npx skills": { source: "untracked" },
2363
2041
  "repo skill": { source: "repo" },
2364
- "repo skill, in library": { source: "repo", library: "same" },
2365
- "repo skill, differs from library": { source: "repo", library: "differs" }
2042
+ "repo skill, in library": { source: "repo" },
2043
+ "repo skill, differs from library": { source: "repo" }
2366
2044
  };
2367
- const state = git2 ? git2.of(relativeTo(root, s.path)) : null;
2045
+ const state = git ? git.of(relativeTo(root, s.path)) : null;
2046
+ const others = otherCopies(root, s.name, s.location);
2368
2047
  const own = other[s.state]?.source === "untracked" && (state === "committed" || state === "changed") ? { source: "repo" } : {};
2369
- const blind = s.state === "folder missing" ? [] : s.visibility.filter((v) => v.paths === 0).map((v) => v.id);
2370
- const uncommitted = blind.length ? linkEverywhere(root, s.name, s.location, { dryRun: true }).uncommitted : [];
2371
- const unshared = blind.filter((id) => uncommitted.includes(harness(id).projectDirs[0]));
2372
2048
  return {
2373
2049
  name: s.name,
2374
2050
  source: "lib",
@@ -2376,18 +2052,19 @@ function localSkill(root, s, git2) {
2376
2052
  path: s.path,
2377
2053
  // Agents that actually load it here: visibility lists every agent you use, with `paths: 0` for the blind ones.
2378
2054
  agents: s.visibility.filter((v) => v.paths > 0).map((v) => v.id),
2379
- ...s.managed ? { library: "same", version: s.version ?? void 0, ...managed[s.state] } : { ...other[s.state], ...own },
2380
- ...unshared.length && { unshared },
2055
+ ...s.managed ? { version: s.version ?? void 0, ...managed[s.state] } : { ...other[s.state], ...own },
2381
2056
  ...s.source ? { origin: `npx skills: ${s.source}` } : {},
2382
- added: addedAt(s.path),
2057
+ ...others.length ? { others } : {},
2383
2058
  git: state
2384
2059
  };
2385
2060
  }
2061
+ function otherCopies(root, name, dir) {
2062
+ return foldersOf(root, name).real.map((d) => relativeTo(root, d)).filter((d) => d !== dir);
2063
+ }
2386
2064
  function installedAt(path) {
2387
2065
  try {
2388
2066
  const st = lstatSync(path);
2389
- const at = st.birthtimeMs > 0 ? st.birthtime : st.mtime;
2390
- return new Date(at.getTime() - at.getTimezoneOffset() * 6e4).toISOString().slice(0, 16).replace("T", " ");
2067
+ return localStamp((st.birthtimeMs > 0 ? st.birthtime : st.mtime).getTime());
2391
2068
  } catch {
2392
2069
  return void 0;
2393
2070
  }
@@ -2397,7 +2074,7 @@ function machineSkill(s, kept) {
2397
2074
  return {
2398
2075
  name: s.name,
2399
2076
  source: global ? "global" : s.kind === "plugin" ? "plugin" : s.kind === "claude.ai" ? "claude.ai" : s.kind === "system" ? "system" : s.kind === "skilllib" ? "skilllib" : s.kind === "built-in" && s.harnesses[0] === "grok" ? "grok" : "cursor",
2400
- where: global || s.kind === "skilllib" ? tildify4(dirname(s.path)) : s.kind === "claude.ai" ? "claude.ai account" : s.kind === "built-in" ? `${s.origin} built-in` : s.origin,
2077
+ where: global || s.kind === "skilllib" ? tildify2(dirname(s.path)) : s.kind === "claude.ai" ? "claude.ai account" : s.kind === "built-in" ? `${s.origin} built-in` : s.origin,
2401
2078
  ...s.kind === "skills.sh" ? { origin: `npx skills: ${s.origin}` } : {},
2402
2079
  path: s.path,
2403
2080
  links: s.links,
@@ -2429,13 +2106,6 @@ function pluginOf(p, repoOf) {
2429
2106
  function refOf(p) {
2430
2107
  return { id: p.id, scope: p.scope === "claude.ai" ? null : p.scope, ...p.projectPath ? { projectPath: p.projectPath } : {}, synced: p.scope === "claude.ai", ...p.settingsOnly ? { settingsOnly: true } : {} };
2431
2108
  }
2432
- function dupesOf(r, root, git2 = null) {
2433
- const out = /* @__PURE__ */ new Map();
2434
- for (const p of r.plans) out.set(p.name, { steps: p.steps.map((s) => describeStep(s, root)), git: gitVisibleSteps(p, git2).map((s) => describeStep(s, root)) });
2435
- for (const c of r.conflicts)
2436
- out.set(c.name, { steps: [], git: [], differ: c.copies.map((x) => ({ dir: x.dir, label: root ? copyLabel(c, x.dir) : tildify4(x.dir), runs: x.runs, canWin: !c.managed || x.dir === c.managed })) });
2437
- return out;
2438
- }
2439
2109
  function loadWorld() {
2440
2110
  forgetLatest();
2441
2111
  forgetClaudeBinary();
@@ -2450,7 +2120,8 @@ function loadWorld() {
2450
2120
  };
2451
2121
  const visible = visibleProjects();
2452
2122
  const cwd = here ? visible.find((p) => real(p) === real(here)) ?? here : null;
2453
- const roots = [.../* @__PURE__ */ new Set([...cwd ? [cwd] : [], ...visible])];
2123
+ const listed = [.../* @__PURE__ */ new Set([...cwd ? [cwd] : [], ...visible])];
2124
+ const roots = readableRoots(listed).readable;
2454
2125
  const names = uniqueNames(roots);
2455
2126
  const descriptions = {};
2456
2127
  const origins = libraryOrigins();
@@ -2458,18 +2129,13 @@ function loadWorld() {
2458
2129
  descriptions[s.name] = s.description;
2459
2130
  return { name: s.name, latest: latestVersion(s.name)?.version ?? 1, origin: origins[s.name] };
2460
2131
  });
2132
+ const libraryNames = new Set(library.map((l) => l.name));
2461
2133
  const kept = keptGlobal();
2462
2134
  const cursorList = agents.includes("cursor") ? cursorPlugins() : [];
2463
- const cursorPlugin = new Map(cursorPluginSkills(cursorList).map((s) => [s.name, s.plugin]));
2464
- const globalDupes = dupesOf(planGlobalTidy({ enabled: agents }), null);
2465
2135
  const sourced = machineSkills(agents);
2466
2136
  const machine = sourced.map((s) => {
2467
2137
  descriptions[s.name] ??= s.description;
2468
- const m = machineSkill(s, kept);
2469
- if (m.source !== "global" || m.broken) return m;
2470
- const dupes = globalDupes.get(m.name);
2471
- const plugin = cursorPlugin.get(m.name);
2472
- return { ...m, ...dupes ? { dupes } : {}, ...plugin ? { cursorPlugin: plugin } : {} };
2138
+ return machineSkill(s, kept);
2473
2139
  });
2474
2140
  const loadedHere = (root) => {
2475
2141
  const loaded = skillsLoadedIn(root, sourced, agents);
@@ -2477,21 +2143,17 @@ function loadWorld() {
2477
2143
  for (const s of loaded) descriptions[s.name] ??= s.description;
2478
2144
  return { machine: loaded.map((s) => machine[sourced.indexOf(s)] ?? machineSkill(s, kept)) };
2479
2145
  };
2146
+ const statuses = new Map(roots.map((root) => [root, projectStatus(root)]));
2480
2147
  const projects = roots.map((root) => {
2481
- const status = projectStatus(root);
2148
+ const status = statuses.get(root);
2482
2149
  const hasManifest = existsSync3(join2(root, "skilllib.json"));
2483
- const git2 = status.length || hasManifest ? gitInfo(root) : null;
2150
+ const git = status.length || hasManifest ? gitInfo(root) : null;
2484
2151
  for (const s of status) if (!descriptions[s.name] && !s.state.includes("missing")) descriptions[s.name] = readSkillInfo(s.path).description;
2485
- const dupes = status.length ? dupesOf(planProjectTidy(root, { enabled: agents }), root, git2) : /* @__PURE__ */ new Map();
2486
2152
  return {
2487
2153
  name: names.get(root),
2488
2154
  path: root,
2489
- manifest: hasManifest ? git2 ? git2.of("skilllib.json") : "no git" : "none",
2490
- skills: status.map((s) => {
2491
- const d = dupes.get(s.name);
2492
- const plugin = cursorPlugin.get(s.name);
2493
- return { ...localSkill(root, s, git2), ...d ? { dupes: d } : {}, ...plugin ? { cursorPlugin: plugin } : {} };
2494
- }),
2155
+ manifest: hasManifest ? git ? git.of("skilllib.json") : "no git" : "none",
2156
+ skills: status.map((s) => localSkill(root, s, git)),
2495
2157
  ...loadedHere(root)
2496
2158
  };
2497
2159
  });
@@ -2500,19 +2162,19 @@ function loadWorld() {
2500
2162
  const plugins = [...installedPlugins(), ...cursorList].map((p) => pluginOf(p, repoOf));
2501
2163
  return {
2502
2164
  agents,
2503
- cwd: cwd ? names.get(cwd) : null,
2165
+ cwd: cwd && names.has(cwd) ? names.get(cwd) : null,
2504
2166
  projects,
2505
2167
  machine,
2506
2168
  plugins,
2507
2169
  library,
2508
2170
  usage: {},
2509
2171
  descriptions,
2510
- backups: backups.map((b) => ({ name: b.name, from: tildify4(b.from), at: b.movedAt })),
2511
- roots: readConfig().roots,
2512
- hidden: readConfig().hidden,
2172
+ backups: backups.map((b) => ({ name: b.name, from: tildify2(b.from), at: b.movedAt })),
2173
+ ...(({ roots: roots2, hidden }) => ({ roots: roots2, hidden }))(readConfig()),
2513
2174
  agentsChosen: harnessesChosen(),
2514
2175
  agentSkill: agentSkillState(agents),
2515
- ops: realOps(new Map(projects.map((p) => [p.name, p.path])), backups)
2176
+ issues: findIssues(listed, sourced, libraryNames, (root) => statuses.get(root)),
2177
+ ops: realOps(new Map(projects.map((p) => [p.name, p.path])), backups, sourced, libraryNames)
2516
2178
  };
2517
2179
  }
2518
2180
  async function loadUsage(w) {
@@ -2522,7 +2184,11 @@ async function loadUsage(w) {
2522
2184
  const days = {};
2523
2185
  const midnight = (t) => new Date(t).setHours(0, 0, 0, 0);
2524
2186
  const today = midnight(Date.now());
2187
+ const totals = {};
2525
2188
  for (const use of await scanUsage(30)) {
2189
+ const skill = use.skill.split(":").pop();
2190
+ totals[skill] = (totals[skill] ?? 0) + 1;
2191
+ if (skill !== use.skill) totals[use.skill] = (totals[use.skill] ?? 0) + 1;
2526
2192
  const root = projectOf(use.cwd);
2527
2193
  const repo = root ? repoOf.get(root) : void 0;
2528
2194
  const day = 29 - Math.round((today - midnight(Date.parse(use.at))) / 864e5);
@@ -2530,18 +2196,7 @@ async function loadUsage(w) {
2530
2196
  (usage[repo] ??= {})[use.skill] = (usage[repo][use.skill] ?? 0) + 1;
2531
2197
  ((days[repo] ??= {})[use.skill] ??= new Array(30).fill(0))[day] += 1;
2532
2198
  }
2533
- return { usage, days };
2534
- }
2535
- function git(root, args) {
2536
- try {
2537
- return execFileSync("git", args, { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
2538
- } catch {
2539
- return "";
2540
- }
2541
- }
2542
- function remoteLabel(url) {
2543
- if (!url) return void 0;
2544
- return url.replace(/^[a-z+]+:\/\//, "").replace(/^[^@/]+@/, "").replace(/:(?!\d)/, "/").replace(/\.git$/, "");
2199
+ return { usage, days, totals };
2545
2200
  }
2546
2201
  function said3(c, repo) {
2547
2202
  if (c.action === "skipped") return `${c.name}: ${c.reason ?? "skipped"}`;
@@ -2550,10 +2205,9 @@ function said3(c, repo) {
2550
2205
  const done = back ? `Pinned ${c.name} to v${c.to} in ${repo} (was v${c.from})` : `${c.action === "installed" ? "Added" : c.action === "reset" ? "Reset" : "Updated"} ${c.name}${c.to ? ` v${c.to}` : ""} in ${repo}`;
2551
2206
  return c.backedUp ? `${done} (your edits are in Settings \u203A Backups)` : done;
2552
2207
  }
2553
- function realOps(roots, backups) {
2208
+ function realOps(roots, backups, machine, libraryNames) {
2554
2209
  const rootOf = (repo) => roots.get(repo);
2555
2210
  const find = (repo, name) => projectStatus(rootOf(repo)).find((s) => s.name === name);
2556
- const info = /* @__PURE__ */ new Map();
2557
2211
  const result = (c, repo) => c.action === "skipped" ? failed(said3(c, repo)) : said3(c, repo);
2558
2212
  const copyDir = (repo, name) => readManifest(rootOf(repo)).skills[name]?.dir ?? PROJECT_SKILLS_DIR;
2559
2213
  const withLinks = (c, repo, name) => {
@@ -2595,9 +2249,8 @@ function realOps(roots, backups) {
2595
2249
  return {
2596
2250
  label: `Also link ${new Set(held.map((h) => h.name)).size === 1 ? "it" : "them"} in ${dirs.join(", ")}`,
2597
2251
  preview: `${dirs.join(", ")} ${dirs.length === 1 ? "is" : "are"} committed in ${repos.join(", ")}: the links will show in git status. skilllib remembers this for ${repos.join(", ")}.`,
2598
- run: (w) => {
2252
+ run: () => {
2599
2253
  for (const repo of repos) allowTrackedLinks(rootOf(repo));
2600
- if (held.length === 1) return w.ops.link(held[0].repo, held[0].name, true);
2601
2254
  const results = held.map((h) => ({ ...h, r: linkIn(h.repo, h.name, true) }));
2602
2255
  const linked = results.filter((x) => x.r.created.length);
2603
2256
  const names = [...new Set(linked.map((x) => x.name))];
@@ -2632,81 +2285,27 @@ function realOps(roots, backups) {
2632
2285
  if (!s) return failed(`${name} isn't in ${repo}`);
2633
2286
  const committed = !s.managed && ["committed", "changed"].includes(gitInfo(rootOf(repo))?.of(relativeTo(rootOf(repo), s.path)) ?? "");
2634
2287
  if (s.state.startsWith("repo skill") || committed) return failed(`${name} is the repo's own (committed by your team): skilllib doesn't delete it`);
2635
- if (!s.managed) return removeUntracked(rootOf(repo), name, s.path), `Removed ${name} from ${repo} (in Settings \u203A Backups)`;
2636
- return orForce(removeSkill(rootOf(repo), name), repo, "Remove it anyway", () => removeSkill(rootOf(repo), name, { force: true }));
2637
- },
2638
- restore: (repo, name) => {
2639
- const c = syncProject(rootOf(repo)).find((x) => x.name === name);
2640
- return c ? result(c, repo) : `${name} is already in place`;
2288
+ const others = otherCopies(rootOf(repo), name, s.location);
2289
+ const left = (r) => typeof r === "string" && others.length ? `${r}; the copy in ${others.join(", ")} stays` : r;
2290
+ if (!s.managed) return removeUntracked(rootOf(repo), name, s.path), left(`Removed ${others.length ? `${s.location}/${name}` : name} from ${repo} (in Settings \u203A Backups)`);
2291
+ return left(orForce(removeSkill(rootOf(repo), name), repo, "Remove it anyway", () => removeSkill(rootOf(repo), name, { force: true })));
2641
2292
  },
2642
2293
  update: (repo, name) => {
2643
2294
  const c = updateProject(rootOf(repo), [name])[0];
2644
2295
  return c ? orForce(c, repo, "Update anyway", () => updateProject(rootOf(repo), [name], { force: true })[0]) : `${name} is up to date`;
2645
2296
  },
2646
- saveEdits: (repo, name) => {
2647
- const s = find(repo, name);
2648
- const saved = toLibrary(s.path);
2649
- addSkill(rootOf(repo), name);
2650
- return saved;
2651
- },
2652
- discardEdits: (repo, name) => {
2653
- const s = find(repo, name);
2654
- removeUntracked(rootOf(repo), name, s.path);
2655
- return withLinks(addSkill(rootOf(repo), name, s.managed && s.version ? { version: s.version } : {}), repo, name);
2656
- },
2657
- link: (repo, name, allow = false) => {
2658
- const r = linkIn(repo, name, allow);
2659
- const message = linkSaid(name, r);
2660
- if (!r.blocked.length || allow) return r.created.length ? message : failed(message);
2661
- return { message: `${message}; not linked in ${r.blocked.join(", ")}: git tracks ${r.blocked.length === 1 ? "it" : "them"}`, then: allowTracked([{ repo, name, blocked: r.blocked }]) };
2662
- },
2663
- track: (repo, name) => withLinks(addSkill(rootOf(repo), name), repo, name),
2664
- importLocal: (repo, name) => {
2665
- const s = find(repo, name);
2666
- importSkill(s.path);
2667
- return withLinks(addSkill(rootOf(repo), name), repo, name);
2668
- },
2669
2297
  copyToLibrary: (repo, name) => toLibrary(find(repo, name).path),
2670
- tidy: (repo, name, { keep, allowGit = false } = {}) => {
2671
- const root = repo === null ? null : rootOf(repo);
2672
- const opts = keep ? { keep: { [name]: keep } } : {};
2673
- const plan = (root ? planProjectTidy(root, opts) : planGlobalTidy(opts)).plans.find((p) => p.name === name);
2674
- if (!plan) return `${name}: nothing to tidy`;
2675
- const r = applyTidy(plan, { git: allowGit ? "go" : "keep" });
2676
- const steps = (list) => list.map((s) => describeStep(s, root)).join(", ");
2677
- if (!r.held.length) return r.applied.length ? `${name}: ${steps(r.applied)}` : `${name}: nothing changed`;
2678
- return {
2679
- message: `${name}: ${r.applied.length ? `${steps(r.applied)}; ` : ""}held back ${steps(r.held)}, because git tracks it`,
2680
- then: {
2681
- label: "Also change what git tracks",
2682
- preview: `${steps(r.held)}: these changes will show in git status in ${repo}. Replaced copies go to Settings \u203A Backups.`,
2683
- run: (w) => w.ops.tidy(repo, name, { keep, allowGit: true })
2684
- }
2685
- };
2686
- },
2687
- unloadGlobal: (m) => {
2688
- if (!m.broken && !latestVersion(m.name)) importSkill(m.path);
2689
- const r = unloadGlobal(m.path, m.links);
2690
- return r.ok ? `${m.name} no longer loads globally (in Settings \u203A Backups)` : failed(`${m.name}: ${r.reason}`);
2691
- },
2692
2298
  deleteGlobal: (m) => {
2693
2299
  const r = deleteGlobal(m.path, m.links);
2694
2300
  return r.ok ? m.broken ? `Removed the broken link ${m.name}` : `Deleted ${m.name} (in Settings \u203A Backups)` : failed(`${m.name}: ${r.reason}`);
2695
2301
  },
2696
2302
  keepGlobal: (name, keep) => (setKeepGlobal([name], keep), keep ? `${name} marked as global on purpose` : `${name} is no longer marked as global on purpose`),
2697
- linkGlobal: (m, agents) => {
2698
- const r = linkGlobal(m.path, agents);
2699
- const linked = r.linked.map(tildify4).join(", ");
2700
- const skipped = r.skipped.length ? `; ${r.skipped.map(tildify4).join(", ")} already holds a different skill` : "";
2701
- const message = `${m.name}: ${linked ? `linked ${linked}` : "nothing linked"}${skipped}`;
2702
- return !linked && skipped ? failed(message) : message;
2703
- },
2704
2303
  moveGlobal: (skills, repos, group) => {
2705
2304
  const moved = [];
2706
2305
  const notes = [];
2707
2306
  const held = [];
2708
2307
  const repoRoots = repos.map(rootOf);
2709
- for (const mv of moveGlobalSkills(skills, repoRoots, { group, label: (root) => repos[repoRoots.indexOf(root)] })) {
2308
+ for (const mv of moveGlobalSkills(skills, repoRoots, { group, label: (root) => repos[repoRoots.indexOf(root)], allowBlind: true })) {
2710
2309
  if (!mv.moved) {
2711
2310
  notes.push(`${mv.name}: ${mv.reason}`);
2712
2311
  continue;
@@ -2744,18 +2343,14 @@ function realOps(roots, backups) {
2744
2343
  const r = createSkill(name, "");
2745
2344
  return r.ok ? `Created ${name} in your library: edit its SKILL.md from Your skills` : failed(`${name}: ${r.reason}`);
2746
2345
  },
2747
- pluginOffHere: (repo, id) => {
2748
- const root = rootOf(repo);
2749
- const plugin = claudePlugins(skillsLoadedIn(root, machineSkills()), root).find((p) => p.id === id);
2750
- if (!plugin) return failed(`${id} isn't on in ${repo}`);
2751
- const r = turnOffIn(plugin, root);
2752
- return r.ok ? r.message : failed(r.message);
2753
- },
2754
2346
  replacePlugin: (p, repos) => {
2755
2347
  const id = p.id;
2348
+ const clashes = p.skills.filter((s) => {
2349
+ const mine = treeHash(librarySkillDir(s.name));
2350
+ return mine !== null && mine !== treeHash(s.path);
2351
+ });
2352
+ if (clashes.length) return failed(`${id} kept: your library has different ${clashes.map((s) => s.name).join(", ")}; update or delete ${clashes.length === 1 ? "it" : "them"} first`);
2756
2353
  const imported = p.skills.map((s) => ({ name: s.name, status: importSkill(s.path).status }));
2757
- const clashes = imported.filter((r) => r.status === "exists").map((r) => r.name);
2758
- if (clashes.length) return failed(`${id} kept: your library has different ${clashes.join(", ")}; update or delete ${clashes.length === 1 ? "it" : "them"} first`);
2759
2354
  for (const r of imported) if (r.status !== "unchanged") recordOrigin(r.name, `plugin: ${id}`);
2760
2355
  const added = repos.length ? addMany(repos, p.skills.map((s) => s.name)) : { result: "", skipped: [], notes: [] };
2761
2356
  if (added.skipped.length) return failed(`${id} kept: ${added.skipped.join("; ")}`);
@@ -2805,7 +2400,7 @@ function realOps(roots, backups) {
2805
2400
  restoreBackup: (i) => {
2806
2401
  const b = backups[i];
2807
2402
  const r = b.kind === "plugin" ? restorePlugin(b) : restoreBackup(b);
2808
- return r.ok ? `Restored ${b.name} to ${tildify4(r.to)}` : failed(`${b.name}: ${r.reason}`);
2403
+ return r.ok ? `Restored ${b.name} to ${tildify2(r.to)}` : failed(`${b.name}: ${r.reason}`);
2809
2404
  },
2810
2405
  setAgents: (ids) => (setHarnesses(ids), "Saved your agents"),
2811
2406
  installVersion: (repo, name, version) => withLinks(addSkill(rootOf(repo), name, { version, force: true }), repo, name),
@@ -2814,40 +2409,37 @@ function realOps(roots, backups) {
2814
2409
  addRoot: (path) => {
2815
2410
  const root = addRoot(path);
2816
2411
  discoverProjects();
2817
- return `Scanning ${tildify4(root)} for repos`;
2412
+ return `Scanning ${tildify2(root)} for repos`;
2818
2413
  },
2819
- removeRoot: (path) => (removeRoot(path), `Stopped scanning ${tildify4(path)}; repos already found stay until you hide them`),
2414
+ removeRoot: (path) => (removeRoot(path), `Stopped scanning ${tildify2(path)}; repos already found stay until you hide them`),
2820
2415
  rescan: () => (discoverProjects(), "Looked for repos in your project folders again"),
2821
2416
  hide: (repo) => (setHidden(rootOf(repo), true), `Hid ${repo}: Settings \u203A Hidden repos brings it back`),
2822
- unhide: (path) => (setHidden(path, false), `${tildify4(path)} is back in the list`),
2417
+ unhide: (path) => (setHidden(path, false), `${tildify2(path)} is back in the list`),
2823
2418
  openFolder: (path) => {
2824
2419
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
2825
2420
  spawn(cmd, [path], { detached: true, stdio: "ignore" }).unref();
2826
- return `Opened ${tildify4(path)}`;
2421
+ return `Opened ${tildify2(path)}`;
2827
2422
  },
2828
2423
  copy: (text, what) => {
2829
2424
  const r = copyText(text);
2830
- return r.copied ? `Copied ${what}: paste it into Claude Code, Cursor, Codex or Grok` : `Couldn't reach the clipboard; ${what} is in ${tildify4(r.savedTo)}`;
2425
+ return r.copied ? `Copied ${what}: paste it into Claude Code, Cursor, Codex or Grok` : `Couldn't reach the clipboard; ${what} is in ${tildify2(r.savedTo)}`;
2831
2426
  },
2832
2427
  installAgentSkill: () => {
2833
2428
  const r = installAgentSkill();
2834
- return r.ok ? `Your agents can now use skilllib (${r.dirs.map(tildify4).join(", ")})` : failed(`skilllib skill: ${r.reason}`);
2429
+ return r.ok ? `Your agents can now use skilllib (${r.dirs.map(tildify2).join(", ")})` : failed(`skilllib skill: ${r.reason}`);
2835
2430
  },
2836
2431
  removeAgentSkill: () => {
2837
2432
  const removed = removeAgentSkill();
2838
- return removed.length ? `Removed the skilllib skill (${removed.map(tildify4).join(", ")})` : failed("The skilllib skill isn't installed");
2433
+ return removed.length ? `Removed the skilllib skill (${removed.map(tildify2).join(", ")})` : failed("The skilllib skill isn't installed");
2839
2434
  },
2840
- repoInfo: (repo) => {
2841
- if (!info.has(repo)) {
2842
- const root = rootOf(repo);
2843
- const status = git(root, ["status", "--porcelain"]);
2844
- info.set(repo, {
2845
- remote: remoteLabel(git(root, ["remote", "get-url", "origin"])),
2846
- branch: git(root, ["rev-parse", "--abbrev-ref", "HEAD"]) || void 0,
2847
- dirty: status ? status.split("\n").length : 0
2848
- });
2849
- }
2850
- return info.get(repo);
2435
+ usageIssues: ({ usage, totals = {} }) => {
2436
+ const repoOf = new Map([...roots].map(([name, root]) => [root, name]));
2437
+ return usageIssues([...roots.values()], machine, libraryNames, {
2438
+ inProject: (root, skill) => usage[repoOf.get(root)]?.[skill] ?? 0,
2439
+ total: (skill) => totals[skill] ?? 0,
2440
+ asUsed: (key) => totals[key] ?? 0,
2441
+ days: 30
2442
+ });
2851
2443
  }
2852
2444
  };
2853
2445
  }