skilllib 1.11.2 → 1.11.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,44 +1,47 @@
1
1
  import {
2
2
  HARNESSES,
3
3
  PROJECT_SKILLS_DIR,
4
+ addRefusal,
4
5
  addRoot,
5
6
  addSkill,
6
- addedAt,
7
7
  agentSkillState,
8
8
  allowTrackedLinks,
9
- applyTidy,
9
+ blindSaid,
10
+ blindWithoutGlobal,
10
11
  claudeBinary,
11
- claudePlugins,
12
- copyLabel,
13
12
  createSkill,
14
- cursorPluginSkills,
15
13
  cursorPlugins,
16
14
  deleteGlobal,
17
15
  deleteLibrarySkill,
18
- describeStep,
19
16
  discoverProjects,
20
17
  enabledHarnesses,
18
+ entryExists,
19
+ findIssues,
20
+ foldersOf,
21
21
  forgetClaudeBinary,
22
22
  forgetLatest,
23
23
  gitInfo,
24
- gitVisibleSteps,
25
24
  harness,
26
25
  harnessesChosen,
27
26
  importSkill,
27
+ inTheWaySaid,
28
28
  installAgentSkill,
29
+ installDirs,
29
30
  installedPlugins,
31
+ isDir,
30
32
  keptGlobal,
33
+ knownProjects,
31
34
  latestVersion,
32
35
  libraryOrigins,
33
36
  librarySkillDir,
34
37
  librarySkills,
35
38
  linkEverywhere,
36
- linkGlobal,
37
39
  listBackups,
40
+ localStamp,
38
41
  machineSkills,
39
42
  moveGlobalSkills,
40
- planGlobalTidy,
41
- planProjectTidy,
43
+ movedTogether,
44
+ notWritable,
42
45
  pluginBackups,
43
46
  projectHere,
44
47
  projectOfFactory,
@@ -47,39 +50,45 @@ import {
47
50
  readConfig,
48
51
  readManifest,
49
52
  readSkillInfo,
53
+ readableRoots,
54
+ realPath,
50
55
  recordOrigin,
51
56
  relativeTo,
52
57
  relinkDependency,
58
+ rememberProjects,
53
59
  removeAgentSkill,
54
60
  removeRoot,
55
61
  removeSkill,
56
62
  removeUntracked,
57
63
  restoreBackup,
58
64
  restorePlugin,
65
+ runFix,
59
66
  scanUsage,
60
67
  setHarnesses,
61
68
  setHidden,
62
69
  setKeepGlobal,
63
70
  setPluginOn,
71
+ skillOf,
64
72
  skilllibHome,
65
73
  skillsLoadedIn,
66
- syncSkill,
74
+ strandedLinks,
75
+ survives,
67
76
  tildify,
68
- turnOffIn,
77
+ trackBlocker,
78
+ treeHash,
69
79
  uninstallPlugin,
70
- unloadGlobal,
71
80
  updatePlugin,
72
81
  updateProject,
82
+ usageIssues,
73
83
  versionHistory,
74
- visibilityOf,
75
84
  visibleProjects
76
- } from "./chunk-OFWHWIER.js";
85
+ } from "./chunk-CS6T66EE.js";
77
86
 
78
87
  // src/tui/index.tsx
79
88
  import { render } from "ink";
80
89
 
81
90
  // src/tui/App.tsx
82
- import { useEffect, useRef, useState } from "react";
91
+ import { useEffect, useMemo, useRef, useState } from "react";
83
92
  import { Box as Box2, Text as Text2, useApp, useInput, useWindowSize } from "ink";
84
93
  import { spawnSync as spawnSync2 } from "child_process";
85
94
  import { existsSync as existsSync2 } from "fs";
@@ -135,7 +144,6 @@ function ListPanel({
135
144
  rows,
136
145
  selected,
137
146
  empty,
138
- filter,
139
147
  header,
140
148
  headerHeight = 0
141
149
  }) {
@@ -143,7 +151,7 @@ function ListPanel({
143
151
  const offset = Math.min(Math.max(0, selected - Math.floor(visible / 2)), Math.max(0, rows.length - visible));
144
152
  const shown = rows.slice(offset, offset + visible);
145
153
  const inner = width - 4;
146
- const hint = [filter ?? "", rows.length > visible ? `${selected + 1}/${rows.length}` : ""].filter(Boolean).join(" ");
154
+ const hint = rows.length > visible ? `${selected + 1}/${rows.length}` : "";
147
155
  return /* @__PURE__ */ jsxs(Panel, { title, hint, focused, width, height, children: [
148
156
  header,
149
157
  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 +174,6 @@ function ListPanel({
166
174
  backgroundColor: bg,
167
175
  color: c.color ?? (active && focused ? color.text : void 0),
168
176
  bold: c.bold || active && focused && c.grow,
169
- dimColor: c.dim,
170
177
  wrap: "truncate-end",
171
178
  children: fit(c.text, c.grow ? growWidth : c.width ?? c.text.length)
172
179
  },
@@ -194,16 +201,18 @@ function wrap(text, width, max) {
194
201
  return lines;
195
202
  }
196
203
 
204
+ // src/tui/prompts.ts
205
+ import { dirname } from "path";
206
+
197
207
  // src/review.ts
198
208
  import { spawnSync } from "child_process";
199
209
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
200
210
  import { join } from "path";
201
211
  var EXCERPT_CHARS = 1200;
202
- var tildify2 = (p) => tildify(p);
203
212
  function skillExcerpt(dir, chars) {
204
213
  const file = join(dir, "SKILL.md");
205
214
  if (!existsSync(file)) return { text: "(SKILL.md not found)", files: 0, size: 0 };
206
- const content = readFileSync(file, "utf-8").replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "");
215
+ const content = readFileSync(file, "utf-8").replace(/^\uFEFF/, "").replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "");
207
216
  let files = 0;
208
217
  try {
209
218
  files = readdirSync(dir, { recursive: true, withFileTypes: true }).filter((d) => d.isFile()).length;
@@ -223,7 +232,7 @@ function buildReviewPrompt(skills, ctx) {
223
232
  `- Description: ${s.description || "(none)"}`,
224
233
  `- Source: ${s.source}`,
225
234
  `- Installed via: ${s.installedHow}`,
226
- `- Location: ${tildify2(s.path)}${s.links.length ? ` (also linked from ${s.links.map(tildify2).join(", ")})` : ""}`,
235
+ `- Location: ${tildify(s.path)}${s.links.length ? ` (also linked from ${s.links.map(tildify).join(", ")})` : ""}`,
227
236
  `- Loaded by: ${s.loadedBy}`,
228
237
  ...s.vendor ? [`- Vendor-managed: ${s.vendor}`] : [],
229
238
  ...s.keptGlobal ? ["- I marked this as global on purpose: recommend changing that only for a strong reason (unused, duplicated, low quality)"] : [],
@@ -249,7 +258,7 @@ function buildReviewPrompt(skills, ctx) {
249
258
  - 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
259
 
251
260
  ## My projects
252
- ${ctx.projects.map((p) => `- ${p.name} \u2014 ${tildify2(p.path)}`).join("\n") || "(none)"}
261
+ ${ctx.projects.map((p) => `- ${p.name} \u2014 ${tildify(p.path)}`).join("\n") || "(none)"}
253
262
 
254
263
  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
264
 
@@ -305,16 +314,79 @@ var isFailure = (r) => typeof r !== "string" && "failed" in r;
305
314
  var followUp = (r) => typeof r !== "string" && "then" in r ? r.then : void 0;
306
315
  var said = (r) => typeof r === "string" ? r : r.message;
307
316
  var joined = (results) => {
308
- const message = results.map(said).join(" \xB7 ");
309
- return results.some(isFailure) ? failed(message) : message;
317
+ const bad = results.filter(isFailure);
318
+ const message = [...bad.length && bad.length < results.length ? [`${results.length - bad.length} of ${results.length} done`] : [], ...results.map(said)].join(" \xB7 ");
319
+ return bad.length ? failed(message) : message;
310
320
  };
311
- var SEVERITY_RANK = { problem: 0, warning: 1, hint: 2 };
321
+ var SEVERITY_RANK = { problem: 0, suggestion: 1 };
322
+ function fixOf(c, found) {
323
+ return {
324
+ label: c.label,
325
+ preview: c.hint ? `${c.hint[0].toUpperCase()}${c.hint.slice(1)}. ${found.detail}` : found.detail,
326
+ run: () => {
327
+ const r = runFix(c);
328
+ return r.ok ? r.message : failed(r.message);
329
+ }
330
+ };
331
+ }
332
+ function shortOf(found) {
333
+ const kind = found.id.split(":")[0];
334
+ if (found.id === "unused:library") return "In no repo, unused 30 days";
335
+ if (kind === "plugin") return `Also in \u29C9 ${found.plugin.split("@")[0]}`;
336
+ if (kind === "cursor-plugin") return `Also in Cursor's ${found.plugin}`;
337
+ const short = {
338
+ broken: "Broken link",
339
+ dup: "Loaded twice",
340
+ tidy: "Extra copies",
341
+ conflict: "Copies differ",
342
+ review: "Not reviewed",
343
+ usable: "Not every agent can use it",
344
+ sync: "Folder missing",
345
+ unavailable: "Pinned, not in your library",
346
+ pin: "Pinned version not in your library",
347
+ "out-of-sync": "Not the pinned version",
348
+ update: "Out of date",
349
+ uncommitted: "Commit it first",
350
+ local: "Not in your library",
351
+ adopt: "Not tracked",
352
+ differs: "Differs from your library",
353
+ npx: "From npx skills",
354
+ "plugin-update": "Update available",
355
+ "plugin-unused": "Unused 30 days",
356
+ edited: "Edited here",
357
+ "not-in-lib": "No longer in your library",
358
+ twice: "Repo and global copies",
359
+ unused: "Unused 30 days"
360
+ };
361
+ return short[kind] ?? found.title;
362
+ }
363
+ var views = /* @__PURE__ */ new WeakMap();
364
+ function issueOf(found) {
365
+ let view = views.get(found);
366
+ if (!view) views.set(found, view = viewOf(found));
367
+ return view;
368
+ }
369
+ function viewOf(found) {
370
+ return {
371
+ id: found.id,
372
+ severity: found.severity,
373
+ title: found.title,
374
+ short: shortOf(found),
375
+ detail: found.detail,
376
+ skills: [...new Set(found.at?.flatMap((a) => a.skill ? [a.skill] : []))],
377
+ decision: !found.fix,
378
+ fixes: (found.fix ? [found.fix] : found.choices ?? []).map((c) => fixOf(c, found))
379
+ };
380
+ }
312
381
  function filterOf(source) {
313
382
  return source === "lib" || source === "repo" || source === "untracked" ? "local" : source === "global" || source === "skilllib" ? "global" : source === "plugin" ? "plugins" : "vendor";
314
383
  }
315
384
  function project(w, name) {
316
385
  return w.projects.find((p) => p.name === name);
317
386
  }
387
+ function usesKey(m) {
388
+ return m.source === "plugin" ? `${m.where.split("@")[0]}:${m.name}` : m.name;
389
+ }
318
390
  function totalUses(w, name) {
319
391
  return Object.values(w.usage).reduce((n, u) => n + (u[name] ?? 0), 0);
320
392
  }
@@ -334,344 +406,45 @@ function usable(w, projectName) {
334
406
  const uses = (n) => w.usage[projectName]?.[n] ?? 0;
335
407
  return [
336
408
  ...p.skills.map((s) => ({ name: s.name, source: s.source, where: s.dir, agents: s.agents, uses: uses(s.name), local: s })),
337
- ...machineIn(w, p).map((m) => ({ name: m.name, source: m.source, where: m.where, agents: m.agents, uses: uses(m.name), machine: m }))
409
+ ...machineIn(w, p).map((m) => ({ name: m.name, source: m.source, where: m.where, agents: m.agents, uses: uses(usesKey(m)), machine: m }))
338
410
  ];
339
411
  }
340
- function lib(w, name) {
341
- return w.library.find((l) => l.name === name);
412
+ function issuesAt(w, repo, skill) {
413
+ const root = repo === null ? null : project(w, repo).path;
414
+ return w.issues.filter((i) => i.at?.some((a) => a.root === root && a.skill === skill)).map(issueOf);
342
415
  }
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` : "";
416
+ function repoIssues(w, repo) {
417
+ const root = project(w, repo).path;
418
+ return w.issues.filter((i) => i.at?.some((a) => a.root === root)).map(issueOf);
347
419
  }
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
- ];
420
+ function unlisted(w, repo) {
421
+ const root = project(w, repo).path;
422
+ const listed = new Set(usable(w, repo).map((u) => u.name));
423
+ return [...new Set(w.issues.flatMap((i) => (i.at ?? []).filter((a) => a.root === root && a.skill && !listed.has(a.skill)).map((a) => a.skill)))];
381
424
  }
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
- };
425
+ function libraryIssues(w, name) {
426
+ return w.issues.filter((i) => i.id === "unused:library" && i.at?.some((a) => a.skill === name)).map(issueOf);
427
+ }
428
+ function pluginIssues(w, p) {
429
+ const id = p.agent === "cursor" ? pluginName(p) : p.id;
430
+ return w.issues.filter((i) => i.plugin === id && (i.pluginAt === void 0 || i.pluginAt === (p.projectPath ?? null))).map(issueOf);
397
431
  }
398
432
  function globalFixes(m) {
399
433
  return [
400
434
  {
401
435
  label: "Move it to the repos that need it\u2026",
402
- preview: `Pick repos. ${m.name} goes into your library and those repos, and stops loading globally (original backed up).`,
436
+ preview: `Pick repos. ${m.name} goes into your library and those repos, and stops loading globally (every global copy backed up).`,
403
437
  run: () => "",
404
438
  pickRepos: (w, repos) => w.ops.moveGlobal([m], repos)
405
439
  },
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) },
407
- { label: "Delete it", preview: `Remove ${m.path}. A backup is kept in Settings \u203A Backups.`, run: (w) => w.ops.deleteGlobal(m) }
440
+ 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) },
441
+ { label: "Delete it", preview: `Remove ${m.where}/${m.name}. A backup is kept in Settings \u203A Backups.`, run: (w) => w.ops.deleteGlobal(m) }
408
442
  ];
409
443
  }
410
444
  function machineActions(m) {
411
445
  if (m.broken) return [];
412
446
  return m.source === "global" ? globalFixes(m) : m.source === "skilllib" ? [removeAgentSkillFix()] : [];
413
447
  }
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
448
  function agentSkillFix(w) {
676
449
  return {
677
450
  label: w.agentSkill === "outdated" ? "Update the skilllib skill" : "Install the skilllib skill for your agents",
@@ -686,35 +459,11 @@ function removeAgentSkillFix() {
686
459
  run: (w) => w.ops.removeAgentSkill()
687
460
  };
688
461
  }
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
462
  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;
463
+ const bad = fixes.map((fix) => fix.run(w)).filter(isFailure).map(said);
464
+ const ok = fixes.length - bad.length;
715
465
  const message = [
716
466
  `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
467
  bad.length ? `couldn't: ${bad.join("; ")}` : ""
719
468
  ].filter(Boolean).join(" \xB7 ");
720
469
  return bad.length ? failed(message) : message;
@@ -725,13 +474,9 @@ function worst(issues) {
725
474
  function tally(issues) {
726
475
  return {
727
476
  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
477
+ suggestion: issues.filter((i) => i.severity === "suggestion").length
730
478
  };
731
479
  }
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
480
  function deleteLibraryFix(w, names) {
736
481
  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
482
  const repos = [...new Set(tracked.map((t) => t.repo))];
@@ -742,9 +487,9 @@ function deleteLibraryFix(w, names) {
742
487
  label: `Delete ${names.length === 1 ? "" : `all ${names.length} `}from your library${repos.length ? ` and ${repos.length} repo${repos.length === 1 ? "" : "s"}\u2026` : ""}`,
743
488
  preview: [
744
489
  `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.",
490
+ 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
491
  edited.length ? `${edited.join(", ")} ${edited.length === 1 ? "has" : "have"} local edits: backed up too.` : "",
747
- "Every copy goes to Settings \u203A Backups.",
492
+ "Every copy goes to Settings \u203A Backups; where a repo commits its copy, git will see it.",
748
493
  others.length ? `Untracked or the team's own copies in ${others.join(", ")} stay.` : ""
749
494
  ].filter(Boolean).join(" "),
750
495
  run: (w2) => joined(names.map((n) => w2.ops.deleteLibrary(n)))
@@ -773,7 +518,8 @@ function replacePluginFix(p) {
773
518
  `Copies its ${plural(names.length, "skill")} into your library and adds them to the repos you tick (or none),`,
774
519
  synced ? `then turns ${p.id} off in Claude Code. It's synced from your claude.ai account: to delete it for good, remove it there.` : p.settingsOnly ? `then turns ${p.id} off in ~/.claude/settings.json. It isn't installed, only turned on there, so there's nothing to uninstall.` : `then uninstalls ${p.id}${p.repo ? ` from ${p.repo}` : ""} (its saved data is kept).`,
775
520
  p.parts.length ? `It also brings ${p.parts.join(", ")}: those ${offOnly ? "stop" : "go"} too.` : "",
776
- offOnly ? "Turn it back on from Plugins." : "Settings \u203A Backups reinstalls it."
521
+ // A plugin only turned on in settings isn't listed in Plugins once it's off: say where it comes back.
522
+ offOnly ? p.settingsOnly ? "Turn it back on with /plugin in Claude Code." : "Turn it back on from Plugins." : "Settings \u203A Backups reinstalls it."
777
523
  ].filter(Boolean).join(" "),
778
524
  run: () => "",
779
525
  allowNone: true,
@@ -781,23 +527,18 @@ function replacePluginFix(p) {
781
527
  pickRepos: (w, repos) => w.ops.replacePlugin(p, repos)
782
528
  };
783
529
  }
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
530
  function pluginSwitchFix(p, on) {
791
531
  const where = p.repo ? ` in ${p.repo} (${p.scope} install)` : p.scope === "claude.ai" ? " on this machine" : " in every repo";
792
532
  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.`,
533
+ // doctor's wording, so the details list it once when an issue offers it too.
534
+ label: `Turn the plugin ${p.id} ${on ? "on" : "off"}`,
535
+ 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
536
  run: (w) => w.ops.setPlugin(p, on)
796
537
  };
797
538
  }
798
539
  function uninstallPluginFix(p) {
799
540
  return {
800
- label: "Uninstall",
541
+ label: `Uninstall the plugin ${p.id}`,
801
542
  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
543
  run: (w) => w.ops.uninstallPlugin(p)
803
544
  };
@@ -809,58 +550,6 @@ function updatePluginFix(p) {
809
550
  run: (w) => w.ops.updatePlugin(p)
810
551
  };
811
552
  }
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
553
  function originGroup(origin) {
865
554
  if (!origin) return null;
866
555
  const plugin = origin.match(/^plugin: (.+)$/);
@@ -923,8 +612,8 @@ function addRows(w, target, q) {
923
612
  const taken = new Set(target ? project(w, target).skills.map((s) => s.name) : []);
924
613
  const loaded = new Set(target ? usable(w, target).map((u) => u.name) : []);
925
614
  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] ?? ""));
615
+ const groups = target ? assignGroups(w.library.map((l) => groupCandidates(w, l))) : [];
616
+ 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
617
  const skillRow = (l, inGroup) => ({
929
618
  key: `lib:${l.name}`,
930
619
  name: l.name,
@@ -936,7 +625,7 @@ function addRows(w, target, q) {
936
625
  });
937
626
  const library = [];
938
627
  const done = /* @__PURE__ */ new Set();
939
- for (const { l, g } of target ? shown : []) {
628
+ for (const { l, g } of shown) {
940
629
  if (done.has(l.name)) continue;
941
630
  const members = g ? shown.filter((x) => x.g?.key === g.key).map((x) => x.l) : [l];
942
631
  members.forEach((m) => done.add(m.name));
@@ -969,50 +658,53 @@ function addRows(w, target, q) {
969
658
  }
970
659
 
971
660
  // src/tui/prompts.ts
972
- var agentNames2 = (agents) => agents.map((a) => harness(a).name).join(", ") || "none of my agents";
661
+ var agentNames = (agents) => agents.map((a) => harness(a).name).join(", ") || "none of my agents";
973
662
  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
663
  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
664
  function reviewPrompt(w, names, scope, repo) {
665
+ const project2 = repo ? w.projects.find((p) => p.name === repo) : void 0;
666
+ const machine = project2 ? machineIn(w, project2) : w.machine;
976
667
  const skills = names.map((name) => {
977
- const copies = w.machine.filter((m2) => m2.name === name);
978
- const here = repo ? w.projects.find((p) => p.name === repo)?.skills.find((s) => s.name === name) : void 0;
668
+ const copies = machine.filter((m2) => m2.name === name);
669
+ const here = project2?.skills.find((s) => s.name === name);
979
670
  if (here) {
980
- const lib3 = w.library.find((l) => l.name === name);
671
+ const lib2 = w.library.find((l) => l.name === name);
981
672
  return {
982
673
  name,
983
- description: w.descriptions[name] ?? "",
674
+ description: here.description ?? w.descriptions[name] ?? "",
984
675
  source: localSource(here),
985
676
  installedHow: localInstalled(here),
986
677
  path: here.path,
987
678
  links: [],
988
- loadedBy: `${agentNames2(here.agents)} \u2014 in ${repo}`,
679
+ loadedBy: `${agentNames(here.agents)} \u2014 in ${repo}`,
989
680
  vendor: null,
990
681
  usesTotal: w.days ? totalUses(w, name) : null,
991
682
  usesByProject: Object.entries(w.usage).flatMap(([p, u]) => u[name] ? [[p, u[name]]] : []),
992
683
  installedIn: w.projects.filter((p) => p.skills.some((s) => s.name === name)).map((p) => p.name),
993
684
  otherCopies: copies.map((c) => `${c.source} ${c.where}`),
994
- inYourSkills: lib3 ? `yes, v${lib3.latest}` : null,
685
+ inYourSkills: lib2 ? `yes, v${lib2.latest}` : null,
995
686
  keptGlobal: void 0
996
687
  };
997
688
  }
998
689
  const m = copies.find((c) => c.source === "global") ?? copies[0];
999
- const lib2 = w.library.find((l) => l.name === name);
690
+ const lib = w.library.find((l) => l.name === name);
1000
691
  const repos = w.projects.filter((p) => p.skills.some((s) => s.name === name));
1001
692
  const local = repos[0]?.skills.find((s) => s.name === name);
1002
693
  return {
1003
694
  name,
1004
695
  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 ?? ""),
696
+ 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",
697
+ installedHow: m?.origin ?? (m ? m.where : lib ? "created or imported into skilllib" : local ? localInstalled(local) : "committed in the repo"),
698
+ path: m?.path ?? (lib ? dirname(w.ops.libraryFile(name)) : local?.path ?? ""),
1008
699
  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",
700
+ loadedBy: m ? `${agentNames(m.agents)} \u2014 ${w.machine.some((g) => g.path === m.path) ? "in every repo" : `in ${repo}`}` : repos.map((p) => `${agentNames(p.skills.find((s) => s.name === name).agents)} \u2014 in ${p.name}`).join("; ") || "none of my agents",
1010
701
  vendor: m && m.source !== "global" ? "turn it off at its source (/plugin in Claude Code, claude.ai settings, or Cursor)" : null,
1011
- usesTotal: w.days ? totalUses(w, name) : null,
1012
- usesByProject: Object.entries(w.usage).flatMap(([p, u]) => u[name] ? [[p, u[name]]] : []),
702
+ // A plugin's copy is counted under plugin:skill.
703
+ usesTotal: w.days ? totalUses(w, m ? usesKey(m) : name) : null,
704
+ usesByProject: Object.entries(w.usage).flatMap(([p, u]) => /* @__PURE__ */ ((n) => n ? [[p, n]] : [])(u[m ? usesKey(m) : name])),
1013
705
  installedIn: repos.map((p) => p.name),
1014
706
  otherCopies: copies.filter((c) => c !== m).map((c) => `${c.source} ${c.where}`),
1015
- inYourSkills: lib2 ? `yes, v${lib2.latest}` : null,
707
+ inYourSkills: lib ? `yes, v${lib.latest}` : null,
1016
708
  keptGlobal: m?.kept
1017
709
  };
1018
710
  });
@@ -1025,7 +717,7 @@ function reviewPrompt(w, names, scope, repo) {
1025
717
  }
1026
718
  function writeSkillPrompt(w, name, repo) {
1027
719
  const p = repo ? w.projects.find((x) => x.name === repo) : void 0;
1028
- const file = p ? `${p.path}/.claude/skills/${name || "<name>"}/SKILL.md` : w.ops.libraryFile(name || "<name>");
720
+ const file = p ? `${p.path}/${installDirs(w.agents)[0]}/${name || "<name>"}/SKILL.md` : w.ops.libraryFile(name || "<name>");
1029
721
  return `Write a new agent skill${name ? ` named "${name}"` : ""}${p ? ` for the repo at ${p.path}` : " for my skill library"}.
1030
722
 
1031
723
  Create ${file} with:
@@ -1039,7 +731,6 @@ Keep it under 200 lines. ${p ? "skilllib will offer to import it into my library
1039
731
 
1040
732
  // src/tui/App.tsx
1041
733
  import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1042
- var tildify3 = (p) => tildify(p);
1043
734
  var PLACES = [
1044
735
  ["projects", "Projects"],
1045
736
  ["library", "Your skills"],
@@ -1058,13 +749,12 @@ var CHIPS = [
1058
749
  ];
1059
750
  var SEV = {
1060
751
  problem: { icon: "\u2715", color: color.red },
1061
- warning: { icon: "\u26A0", color: color.yellow },
1062
- hint: { icon: "\xB7", color: color.blue }
752
+ suggestion: { icon: "\u26A0", color: color.yellow }
1063
753
  };
1064
754
  var SIDEBAR_AT = 80;
1065
755
  var SIDEBAR_W = 28;
1066
756
  var bySeverity = (a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity];
1067
- function tag(u, w) {
757
+ function tag(u) {
1068
758
  const s = u.local;
1069
759
  if (s?.source === "lib") {
1070
760
  return [`lib v${s.version}`, color.accent];
@@ -1076,7 +766,7 @@ function tag(u, w) {
1076
766
  return [u.source, color.muted];
1077
767
  }
1078
768
  function isNameChar(input, key) {
1079
- return !key.ctrl && !key.meta && /^[a-zA-Z0-9._-]$/.test(input);
769
+ return !key.ctrl && !key.meta && /^[a-zA-Z0-9._-]+$/.test(input);
1080
770
  }
1081
771
  function clip(text, width) {
1082
772
  return text.length > width - 1 ? text.slice(0, width - 2) + "\u2026" : text;
@@ -1092,7 +782,7 @@ function builder(title) {
1092
782
  issues(issues) {
1093
783
  if (!issues.length) return d.body.push({ segs: [["\u2713 No issues", color.green]] });
1094
784
  for (const issue of [...issues].sort(bySeverity)) {
1095
- d.body.push({ segs: [[`${SEV[issue.severity].icon} `, SEV[issue.severity].color], [issue.title, color.text], [issue.decision ? " \xB7 your call" : "", color.faint]] });
785
+ d.body.push({ segs: [[`${SEV[issue.severity].icon} `, SEV[issue.severity].color], [issue.title, color.text], [issue.decision && issue.fixes.length ? " \xB7 your call" : "", color.faint]] });
1096
786
  issue.fixes.forEach((fix, i) => d.body.push({ segs: [], option: d.options.push({ label: fix.label, fix, issue, recommended: !issue.decision && i === 0 }) - 1 }));
1097
787
  }
1098
788
  return 0;
@@ -1129,10 +819,6 @@ function byName(copies) {
1129
819
  for (const c of copies) groups.set(c.name, [...groups.get(c.name) ?? [], c]);
1130
820
  return [...groups.values()].map((g) => g.sort((a, b) => COPY_RANK[a.source] - COPY_RANK[b.source]));
1131
821
  }
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
822
  var BLOCKS = " \u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
1137
823
  function barChart(values, width, height = 3) {
1138
824
  const max = Math.max(1, ...values);
@@ -1171,7 +857,7 @@ function usageLines(w, name, width, repo) {
1171
857
  function skillItem(w, copies, issues, nameW, actions, repo) {
1172
858
  const u = copies[0];
1173
859
  const top = worst(issues);
1174
- const [t, tc] = tag(u, w);
860
+ const [t, tc] = tag(u);
1175
861
  return {
1176
862
  key: u.name,
1177
863
  name: u.name,
@@ -1189,41 +875,70 @@ function skillItem(w, copies, issues, nameW, actions, repo) {
1189
875
  ],
1190
876
  detail: (width) => {
1191
877
  const b = builder(u.name);
1192
- for (const l of wrap(w.descriptions[u.name] || "(no description)", width - 4, 3)) b.line([l, color.muted]);
878
+ for (const l of wrap(u.local?.description || w.descriptions[u.name] || "(no description)", width - 4, 3)) b.line([l, color.muted]);
1193
879
  b.line();
1194
- if (repo && !u.local) b.line(["Loads in every repo", color.text], [u.source === "global" || u.source === "skilllib" ? " issues and cleanup live in Global" : " managed at its source", color.faint]);
880
+ const here = !!u.machine && !w.machine.some((m) => m.path === u.machine.path && m.where === u.machine.where);
881
+ if (repo && !u.local) b.line([here ? "Loads in this repo (its settings turn it on)" : "Loads in every repo", color.text], [u.source === "global" || u.source === "skilllib" ? " issues and cleanup live in Global" : " managed at its source", color.faint]);
1195
882
  else b.issues(issues);
1196
883
  b.actions(actions, width);
1197
884
  b.line();
1198
- for (const segs of usageLines(w, u.name, width, repo)) b.line(...segs);
885
+ for (const segs of usageLines(w, u.machine ? usesKey(u.machine) : u.name, width, repo)) b.line(...segs);
1199
886
  b.line();
1200
887
  b.line(["Loaded from", color.accent]);
1201
888
  for (const c of copies) {
1202
889
  const s = c.local;
1203
890
  const path = s ? `${s.dir}/${c.name}` : c.source === "global" || c.source === "skilllib" ? `${c.where}/${c.name}` : c.where;
1204
891
  const extra = s ? s.missing ? "missing" : s.git ? GIT_LABEL[s.git] : "" : "";
1205
- const [ct, cc] = c === u ? ["", void 0] : tag(c, w);
892
+ const [ct, cc] = c === u ? ["", void 0] : tag(c);
1206
893
  b.line([" ", void 0], [path + " ", color.text], [ct ? ct + " " : "", cc], [extra, color.faint]);
894
+ for (const dir of s?.others ?? []) b.line([" ", void 0], [`${dir}/${c.name} `, color.text], ["another copy", color.faint]);
1207
895
  }
1208
896
  return b.d;
1209
897
  }
1210
898
  };
1211
899
  }
1212
900
  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
- );
901
+ return [
902
+ ...byName(usable(w, p)).map(
903
+ (copies) => skillItem(w, copies, copies.some((c) => c.local) ? issuesAt(w, p, copies[0].name) : [], nameW, skillActions(w, p, copies[0], ui), p)
904
+ ),
905
+ // A link to nothing loads nothing, but its issue still needs a row (and its fix, Fix all).
906
+ ...unlisted(w, p).map((name) => {
907
+ const issues = issuesAt(w, p, name);
908
+ const top = worst(issues);
909
+ return {
910
+ key: name,
911
+ name,
912
+ group: "local",
913
+ issues,
914
+ uses: 0,
915
+ cells: [
916
+ { text: SEV[top.severity].icon, width: 2, color: SEV[top.severity].color },
917
+ { text: clip(name, nameW), width: nameW },
918
+ { text: "\u2013", width: 14, color: color.faint },
919
+ { text: usesText(0), width: 6, color: color.muted },
920
+ { text: " " + top.short, grow: true, color: SEV[top.severity].color }
921
+ ],
922
+ detail: () => {
923
+ const b = builder(name);
924
+ b.issues(issues);
925
+ return b.d;
926
+ }
927
+ };
928
+ })
929
+ ];
1216
930
  }
1217
931
  var reviewOption = (w, name, ui, repo) => ({
1218
932
  label: "Review prompt",
1219
933
  action: () => ui.copy(reviewPrompt(w, [name], `Review the skill ${name}${repo ? ` in ${repo}` : ""}.`, repo), `a review prompt for ${name}`)
1220
934
  });
935
+ var gitSees = (s) => s?.git === "committed" || s?.git === "changed" ? " Git will see it." : "";
1221
936
  function versionsOption(w, repo, name, installed, ui) {
1222
937
  return {
1223
938
  label: "Other versions\u2026",
1224
939
  action: () => ui.menu(
1225
940
  `${name}: versions`,
1226
- w.ops.versions(name).reverse().map((v) => fixOption({ label: `v${v.version} ${v.date}${v.version === installed ? " (installed)" : ""}`, preview: `Install ${name} v${v.version} in ${repo}.`, run: (w2) => w2.ops.installVersion(repo, name, v.version) }))
941
+ w.ops.versions(name).reverse().map((v) => fixOption({ label: `v${v.version} ${v.date}${v.version === installed ? " (installed)" : ""}`, preview: `Install ${name} v${v.version} in ${repo}.${gitSees(project(w, repo).skills.find((x) => x.name === name))}`, run: (w2) => w2.ops.installVersion(repo, name, v.version) }))
1227
942
  )
1228
943
  };
1229
944
  }
@@ -1233,14 +948,15 @@ function skillActions(w, p, u, ui) {
1233
948
  const remove = {
1234
949
  // Its folder gone: all that's left to remove is the skilllib.json entry (the label Health offers too).
1235
950
  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.`,
951
+ 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.` : "") + // A managed skill's removal edits skilllib.json too.
952
+ (gitSees(s) || (s?.source === "lib" && project(w, p).manifest === "committed" ? " Git will see skilllib.json change." : "")),
1237
953
  run: (w2) => w2.ops.remove(p, u.name)
1238
954
  };
1239
955
  if (s?.source === "lib")
1240
956
  return [
1241
957
  // A tracked skill your library doesn't have: nothing to edit there, no versions to pick.
1242
958
  ...inLibrary ? [
1243
- ...s.version < inLibrary.latest ? [fixOption({ label: `Update to v${inLibrary.latest}`, preview: `Replace ${s.dir}/${u.name} with library v${inLibrary.latest}.`, run: (w2) => w2.ops.update(p, u.name) })] : [],
959
+ ...s.version < inLibrary.latest ? [fixOption({ label: `Update to v${inLibrary.latest}`, preview: `Replace ${s.dir}/${u.name} with library v${inLibrary.latest}.${gitSees(s)}`, run: (w2) => w2.ops.update(p, u.name) })] : [],
1244
960
  { label: "Edit in library", action: () => ui.edit(w.ops.libraryFile(u.name)) },
1245
961
  versionsOption(w, p, u.name, s.version, ui)
1246
962
  ] : [],
@@ -1255,22 +971,19 @@ function skillActions(w, p, u, ui) {
1255
971
  ];
1256
972
  if (s)
1257
973
  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
974
  { label: "Edit SKILL.md", action: () => ui.edit(`${s.path}/SKILL.md`) },
1260
975
  fixOption(remove),
1261
976
  reviewOption(w, u.name, ui, p)
1262
977
  ];
1263
- return [{ label: "Open in Global", action: () => ui.openInGlobal(u.name) }, reviewOption(w, u.name, ui)];
978
+ const everywhere = !!u.machine && w.machine.some((m) => m.path === u.machine.path && m.where === u.machine.where);
979
+ return everywhere ? [{ label: "Open in Global", action: () => ui.openInGlobal(u.name) }, reviewOption(w, u.name, ui)] : [reviewOption(w, u.name, ui, p)];
1264
980
  }
1265
981
  function overview(w, p) {
1266
982
  const project2 = project(w, p);
1267
- const info = w.ops.repoInfo(p);
1268
983
  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
984
  const plugins = [...new Set(machineIn(w, project2).filter((m) => m.source === "plugin").map((m) => m.where.split("@")[0]))];
1270
985
  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]]],
986
+ ["Folder", [[tildify(project2.path), color.text]]],
1274
987
  ["Manifest", [manifest]],
1275
988
  ["Plugins", [[plugins.length ? plugins.join(", ") : "none", plugins.length ? color.magenta : color.faint]]]
1276
989
  ];
@@ -1292,7 +1005,7 @@ function InfoGrid({ rows, columns, width }) {
1292
1005
  }
1293
1006
  function projectItems(w) {
1294
1007
  return w.projects.map((p) => {
1295
- const issues = projectIssues(w, p.name).map((x) => x.issue);
1008
+ const issues = repoIssues(w, p.name);
1296
1009
  const t = tally(issues);
1297
1010
  return {
1298
1011
  key: p.name,
@@ -1304,15 +1017,14 @@ function projectItems(w) {
1304
1017
  { text: p.name, width: 18 },
1305
1018
  { text: p.skills.length ? `${p.skills.length} skill${p.skills.length === 1 ? "" : "s"}` : "\u2013", width: 10, color: color.muted },
1306
1019
  { 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 }
1020
+ { text: t.suggestion ? `\u26A0 ${t.suggestion}` : "", grow: true, color: color.yellow }
1309
1021
  ]
1310
1022
  };
1311
1023
  });
1312
1024
  }
1313
1025
  function globalItems(w, nameW, ui) {
1314
- 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));
1026
+ const copies = w.machine.map((m) => ({ name: m.name, source: m.source, where: m.where, agents: m.agents, uses: totalUses(w, usesKey(m)), machine: m }));
1027
+ return byName(copies).map((g) => skillItem(w, g, issuesAt(w, null, g[0].name), nameW, globalActions(w, g[0], ui), null));
1316
1028
  }
1317
1029
  var AT_SOURCE = {
1318
1030
  global: null,
@@ -1340,7 +1052,8 @@ function libraryItems(w, nameW, ui) {
1340
1052
  const uses = totalUses(w, l.name);
1341
1053
  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
1054
  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] }];
1055
+ const issues = libraryIssues(w, l.name);
1056
+ const sev = issues[0] && SEV[issues[0].severity];
1344
1057
  const behind = repos.filter((p) => p.skills.some((s) => s.name === l.name && s.source === "lib" && s.version < l.latest)).length;
1345
1058
  return {
1346
1059
  key: l.name,
@@ -1350,12 +1063,12 @@ function libraryItems(w, nameW, ui) {
1350
1063
  cands: groupCandidates(w, l),
1351
1064
  lib: l,
1352
1065
  cells: [
1353
- { text: issues[0] ? SEV.hint.icon : " ", width: 2, color: color.blue },
1066
+ { text: sev ? sev.icon : " ", width: 2, color: sev?.color },
1354
1067
  { text: clip(l.name, nameW), width: nameW },
1355
1068
  { 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 },
1069
+ { text: clip(`${repos.length} repo${repos.length === 1 ? "" : "s"}${behind ? `, ${behind} behind` : ""}`, 16), width: 16, color: behind ? color.yellow : color.muted },
1357
1070
  { 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 }
1071
+ { text: " " + (issues[0]?.short ?? w.descriptions[l.name] ?? ""), grow: true, color: sev ? sev.color : color.faint }
1359
1072
  ],
1360
1073
  detail: (width) => {
1361
1074
  const b = builder(l.name);
@@ -1373,7 +1086,7 @@ function libraryItems(w, nameW, ui) {
1373
1086
  ...removable.length ? [
1374
1087
  fixOption({
1375
1088
  label: "Remove from repos\u2026",
1376
- preview: `Pick repos to remove ${l.name} from. Your library keeps it.`,
1089
+ preview: `Pick repos to remove ${l.name} from. Your library keeps it. Where a repo commits its copy, git will see it.`,
1377
1090
  run: () => "",
1378
1091
  candidates: () => removable.map((p) => p.name),
1379
1092
  pickRepos: (w2, rs) => joined(rs.map((r) => w2.ops.remove(r, l.name)))
@@ -1390,43 +1103,37 @@ function libraryItems(w, nameW, ui) {
1390
1103
  };
1391
1104
  });
1392
1105
  }
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",
1106
+ function healthItems(w) {
1107
+ return w.issues.map((found) => {
1108
+ const issue = issueOf(found);
1109
+ const sev = SEV[issue.severity];
1110
+ return {
1111
+ key: issue.id,
1112
+ name: issue.title,
1405
1113
  issues: [issue],
1406
1114
  uses: 0,
1407
1115
  cells: [
1408
1116
  { 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 }
1117
+ { text: issue.title, grow: true }
1412
1118
  ],
1413
1119
  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]);
1120
+ const b = builder(issue.short);
1121
+ for (const l of wrap(found.detail, width - 4, Infinity)) b.line([l, color.muted]);
1416
1122
  b.line();
1417
1123
  b.issues([issue]);
1418
1124
  return b.d;
1419
1125
  }
1420
- }
1421
- ];
1126
+ };
1127
+ });
1422
1128
  }
1423
- var pluginW = (nameW) => Math.min(nameW, 17);
1129
+ var pluginCols = (columns, nameW) => columns >= 76 ? { wide: true, name: Math.min(nameW, 17), scope: 19 } : { wide: false, name: 15, scope: 12 };
1424
1130
  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);
1131
+ function pluginItems(w, columns, skillW, ui, issuesOf) {
1132
+ const { wide, name: nameW, scope: scopeW } = pluginCols(columns, skillW);
1427
1133
  return w.plugins.map((p) => {
1428
- const issues = issuesOf2.get(p.key) ?? [];
1134
+ const issues = issuesOf.get(p.key) ?? [];
1429
1135
  const top = worst(issues);
1136
+ const repeats = new Set(issues.flatMap((i) => i.skills)).size;
1430
1137
  const cursor = p.agent === "cursor";
1431
1138
  const uses = cursor ? 0 : pluginUses(w, p);
1432
1139
  const [state, stateColor] = STATE[p.on === null ? "unknown" : p.on ? "on" : "off"];
@@ -1448,11 +1155,12 @@ function pluginItems(w, skillW, ui, issuesOf2) {
1448
1155
  { text: top ? SEV[top.severity].icon : " ", width: 2, color: top ? SEV[top.severity].color : void 0 },
1449
1156
  { text: clip(name, nameW), width: nameW, color: p.on === false ? color.muted : void 0 },
1450
1157
  { 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 },
1158
+ { text: clip(cursor ? "\u2013" : pluginScope(p), scopeW), width: scopeW, color: p.repo ? color.blue : color.muted },
1159
+ ...wide ? [{ text: clip(`${n} skill${n === 1 ? "" : "s"}${p.parts.length ? ` +${p.parts.length}` : ""}`, 13), width: 13, color: color.muted }] : [],
1453
1160
  { 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 }
1161
+ ...wide ? [{ text: cursor ? " \u2013 " : usesText(uses), width: 6, color: color.muted }] : [],
1162
+ // Your skills it repeats, across its issues; else its worst issue.
1163
+ { text: " " + (repeats ? `Repeats ${repeats} of yours` : top?.short ?? ""), grow: true, color: top ? SEV[top.severity].color : color.faint }
1456
1164
  ],
1457
1165
  detail: (width) => {
1458
1166
  const b = builder(p.id);
@@ -1472,9 +1180,9 @@ function pluginItems(w, skillW, ui, issuesOf2) {
1472
1180
  const skills = p.skills.map((s) => cursor || !w.days ? s.name : `${s.name} (${uses2(totalUses(w, pluginSkillKey(p, s.name)))})`);
1473
1181
  wrap(skills.join(" \xB7 ") || "none", width - 16, 4).forEach((l, i) => b.line(label(i ? "" : "Skills"), [l, color.text]));
1474
1182
  b.line(label("Also brings"), [p.parts.join(", ") || "nothing else", p.parts.length ? color.text : color.faint]);
1475
- 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`;
1183
+ const installed = cursor ? "in Cursor (whether it's on is only in Cursor)" : p.scope === "claude.ai" ? "synced from your claude.ai account" : p.settingsOnly ? "not installed: turned on in your settings, loaded from its marketplace's copy" : p.repo ? `${p.scope} scope, in ${p.repo} only` : `user scope, in every repo`;
1476
1184
  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]);
1185
+ b.line(label("Folder"), [tildify(p.path), color.faint]);
1478
1186
  return b.d;
1479
1187
  }
1480
1188
  };
@@ -1492,10 +1200,12 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1492
1200
  const columns = sidebar ? termCols - SIDEBAR_W : termCols;
1493
1201
  const [base, setBase] = useState(initial);
1494
1202
  const [used, setUsed] = useState();
1495
- const world = used ? { ...base, ...used } : base;
1203
+ const withUsage = used ? { ...base, ...used } : base;
1204
+ const usageIssues2 = useMemo(() => withUsage.days ? base.ops.usageIssues(withUsage) : [], [base, used]);
1205
+ const world = { ...withUsage, issues: [...base.issues, ...usageIssues2] };
1496
1206
  useEffect(() => {
1497
1207
  let live = true;
1498
- void loadUsage2?.(base).then((u) => live && setUsed(u));
1208
+ void loadUsage2?.(base).then((u) => live && setUsed(u)).catch(() => live && setToast("\u2717 Couldn't read your Claude Code transcripts: usage isn't shown"));
1499
1209
  return () => {
1500
1210
  live = false;
1501
1211
  };
@@ -1514,8 +1224,9 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1514
1224
  const [modal, setModal] = useState(null);
1515
1225
  const [toast, setToast] = useState("");
1516
1226
  const order = useRef({ key: "", keys: [] });
1517
- const split = termRows >= 30;
1227
+ const split = termRows >= 30 && place !== "settings";
1518
1228
  const nameW = Math.min(22, Math.max(17, Math.floor(columns * 0.22)));
1229
+ const libNameW = columns < 90 ? nameW - 4 : nameW;
1519
1230
  const dashboard = place === "projects" && open !== null;
1520
1231
  const chips = dashboard ? CHIPS : place === "global" ? CHIPS.filter(([c]) => c !== "local") : place === "settings" ? [] : place === "health" ? CHIPS.slice(0, 1) : CHIPS.slice(0, 2);
1521
1232
  const ui = {
@@ -1527,15 +1238,11 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1527
1238
  menu: (title, options) => setModal({ kind: "menu", title, options, cursor: 0 })
1528
1239
  };
1529
1240
  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();
1241
+ const repoIssues2 = new Map(world.projects.map((p) => [p.name, repoIssues(world, p.name)]));
1242
+ const machineIssues = [...new Set(world.machine.map((m) => m.name))].map((name) => [name, issuesAt(world, null, name)]);
1243
+ const healthCount = world.issues.length;
1244
+ const healthWorst = worst(world.issues.map(issueOf));
1245
+ 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
1246
  const inChip = (i, c) => c === "all" || (c === "issues" ? i.issues.length > 0 : i.group === c);
1540
1247
  let items = all.filter((i) => i.header || (!query || matches(i.name, query)) && inChip(i, chip));
1541
1248
  const viewKey = [place, open, chip, query, used ? "usage" : ""].join("|");
@@ -1556,7 +1263,9 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1556
1263
  if (!query) items = [...actionRows(), ...items];
1557
1264
  const cursorKey = `${place}|${open}`;
1558
1265
  const firstSelectable = Math.max(0, items.findIndex((i) => !i.header && !i.action));
1559
- let cursor = Math.min(cursors[cursorKey] ?? firstSelectable, items.length - 1);
1266
+ const saved = cursors[cursorKey];
1267
+ const kept = saved ? items.findIndex((i) => i.key === saved.key) : -1;
1268
+ let cursor = Math.min(kept >= 0 ? kept : saved?.at ?? firstSelectable, items.length - 1);
1560
1269
  if (items[cursor]?.header) cursor = firstSelectable;
1561
1270
  const current = items[cursor];
1562
1271
  const detail = current?.detail?.(columns);
@@ -1575,7 +1284,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1575
1284
  }
1576
1285
  function choose(o) {
1577
1286
  if (o.action) return o.action();
1578
- if (!o.fix) return setToast("Not in the prototype");
1287
+ if (!o.fix) return;
1579
1288
  if (o.fix.pickRepos) {
1580
1289
  const name = current?.name ?? "";
1581
1290
  const only = o.fix.candidates?.(world);
@@ -1602,7 +1311,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1602
1311
  function fixAllList(from) {
1603
1312
  const seen = /* @__PURE__ */ new Set();
1604
1313
  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 }))
1314
+ (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
1315
  );
1607
1316
  }
1608
1317
  function toggleFold(key) {
@@ -1630,7 +1339,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1630
1339
  const label = groupLabel(g, members.map((m) => m.name));
1631
1340
  const issues = members.flatMap((m) => m.issues);
1632
1341
  const top = worst(issues);
1633
- const flagged2 = members.filter((m) => m.issues.length).length;
1342
+ const flagged = members.filter((m) => m.issues.length).length;
1634
1343
  return {
1635
1344
  key: `fold:${g.key}`,
1636
1345
  name: label,
@@ -1641,13 +1350,14 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1641
1350
  cells: [
1642
1351
  { text: top ? SEV[top.severity].icon : " ", width: 2, color: top ? SEV[top.severity].color : void 0 },
1643
1352
  { 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 }
1353
+ // A leading space keeps it apart from a name cut to fit.
1354
+ { text: flagged ? ` ${flagged} to fix` : "", width: 11, color: top ? SEV[top.severity].color : color.faint }
1645
1355
  ],
1646
1356
  detail: (width) => {
1647
1357
  const b = builder(label);
1648
1358
  for (const l of wrap(members.map((m) => m.name).join(", "), width - 4, 4)) b.line([l, color.muted]);
1649
1359
  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]);
1360
+ 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
1361
  b.actions(groupActions(label, members), width);
1652
1362
  return b.d;
1653
1363
  }
@@ -1670,7 +1380,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1670
1380
  preticked: usedIn,
1671
1381
  pickRepos: (w, repos) => w.ops.moveGlobal(mine, repos, label)
1672
1382
  }),
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`) })] : [],
1383
+ ...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`) })] : [],
1674
1384
  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
1385
  ] : [],
1676
1386
  ...libs.length ? [
@@ -1720,7 +1430,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1720
1430
  () => ui.menu(repo, [
1721
1431
  { label: "Open its folder", action: () => setToast(said2(world.ops.openFolder(project(world, repo).path))) },
1722
1432
  { label: "Hide it from the list", action: () => (apply((w) => w.ops.hide(repo)), setOpen(null), setFocus(sidebar ? "sidebar" : "list")) },
1723
- ...skills.length ? [{ label: `Review prompt for its ${skills.length} skills`, action: () => ui.copy(reviewPrompt(world, skills, `Review the skills in ${repo}.`, repo), `a review prompt for ${repo}`) }] : []
1433
+ ...skills.length ? [{ label: `Review prompt for its ${skills.length === 1 ? "skill" : `${skills.length} skills`}`, action: () => ui.copy(reviewPrompt(world, skills, `Review the skills in ${repo}.`, repo), `a review prompt for ${repo}`) }] : []
1724
1434
  ])
1725
1435
  )
1726
1436
  ];
@@ -1743,9 +1453,12 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1743
1453
  i = Math.max(0, Math.min(items.length - 1, i + delta));
1744
1454
  while (items[i]?.header && i > 0 && i < items.length - 1);
1745
1455
  if (items[i]?.header) return;
1746
- setCursors({ ...cursors, [cursorKey]: i });
1456
+ setCursorAt(i);
1747
1457
  setDetailCursor(0);
1748
1458
  }
1459
+ function setCursorAt(i) {
1460
+ setCursors({ ...cursors, [cursorKey]: { key: items[i]?.key ?? "", at: i } });
1461
+ }
1749
1462
  function goPlace(p) {
1750
1463
  setPlace(p);
1751
1464
  setFocus("list");
@@ -1753,7 +1466,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1753
1466
  setChip("all");
1754
1467
  }
1755
1468
  function edit(file, then) {
1756
- if (!existsSync2(file)) return then ? apply(then) : (setBase(reload()), setToast(`${tildify3(file)} doesn't exist`));
1469
+ if (!existsSync2(file)) return then ? apply(then) : (setBase(reload()), setToast(`${tildify(file)} doesn't exist`));
1757
1470
  const editor = process.env.VISUAL || process.env.EDITOR || (process.platform === "win32" ? "notepad" : "vi");
1758
1471
  void suspendTerminal(() => {
1759
1472
  if (process.platform === "win32") spawnSync2(`${editor} "${file}"`, { shell: true, stdio: "inherit" });
@@ -1762,11 +1475,19 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1762
1475
  const fresh = reload();
1763
1476
  if (then) return apply(then, fresh);
1764
1477
  setBase(fresh);
1765
- setToast(`Saved ${tildify3(file)}`);
1478
+ setToast(`Saved ${tildify(file)}`);
1766
1479
  });
1767
1480
  }
1768
- function askForFolder(title) {
1769
- setModal({ kind: "input", title, value: "", placeholder: "~/Projects", submit: (v) => apply((w) => w.ops.addRoot(v)) });
1481
+ function askForFolder(title, problem) {
1482
+ const submit = (v) => apply((w) => {
1483
+ try {
1484
+ return w.ops.addRoot(v);
1485
+ } catch (e) {
1486
+ askForFolder(title, e.message);
1487
+ throw e;
1488
+ }
1489
+ });
1490
+ setModal({ kind: "input", title: problem ? `${problem}. ${title}` : title, value: "", placeholder: "~/Projects", submit });
1770
1491
  }
1771
1492
  useEffect(() => {
1772
1493
  const folder = () => !initial.roots.length && askForFolder("Where are your projects? skilllib finds the git repos in there");
@@ -1810,16 +1531,16 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1810
1531
  ...world.roots.map(
1811
1532
  (r) => row(
1812
1533
  `root:${r}`,
1813
- tildify3(r),
1534
+ tildify(r),
1814
1535
  "scanned for git repos",
1815
- () => ui.menu(tildify3(r), [
1536
+ () => ui.menu(tildify(r), [
1816
1537
  { 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) })
1538
+ 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
1539
  ])
1819
1540
  )
1820
1541
  ),
1821
1542
  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))))] : [],
1543
+ ...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
1544
  header("#backups", "Backups"),
1824
1545
  ...world.backups.length ? world.backups.map(
1825
1546
  (b, i) => row(`backup:${i}`, `\u21BA ${b.name}`, `${b.from} \xB7 ${b.at}`, () => apply((w) => w.ops.restoreBackup(i)))
@@ -1869,22 +1590,22 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
1869
1590
  if (key.escape) return setSideQuery("");
1870
1591
  return;
1871
1592
  }
1593
+ if (!sidebar && (key.leftArrow || key.rightArrow)) {
1594
+ const i = PLACES.findIndex(([p]) => p === place) + (key.leftArrow ? -1 : 1);
1595
+ if (PLACES[i]) goPlace(PLACES[i][0]);
1596
+ return;
1597
+ }
1872
1598
  const fold = current?.fold;
1873
1599
  if (fold && !fold.open && (key.rightArrow || key.return || input === " ")) return toggleFold(fold.key);
1874
1600
  if (fold?.open && (key.leftArrow || input === " ")) return toggleFold(fold.key);
1875
1601
  if (current?.inFold && key.leftArrow) {
1876
1602
  toggleFold(current.inFold);
1877
- return setCursors({ ...cursors, [cursorKey]: items.findIndex((i) => i.fold?.key === current.inFold) });
1603
+ return setCursorAt(items.findIndex((i) => i.fold?.key === current.inFold));
1878
1604
  }
1879
1605
  if (sidebar && key.leftArrow) return setFocus("sidebar");
1880
1606
  if (sidebar && key.rightArrow && detail) return setFocus("detail"), setDetailCursor(0);
1881
1607
  if (key.upArrow) return move(-1);
1882
1608
  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
1609
  if (key.escape) {
1889
1610
  if (query) return setQuery("");
1890
1611
  if (sidebar) return setFocus("sidebar");
@@ -2020,17 +1741,17 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
2020
1741
  if (isNameChar(input, key)) return setModal({ ...m, query: m.query + input.toLowerCase(), cursor: 0 });
2021
1742
  }
2022
1743
  }
2023
- const bottomH = 1;
1744
+ const toastLines = toast && modal?.kind !== "confirm" ? wrap(toast, termCols - 2, 3).map((l) => " " + l) : [];
1745
+ const bottomH = Math.max(1, toastLines.length);
2024
1746
  const confirmLines = modal?.kind === "confirm" ? wrap(modal.fix.preview, columns - 6, 6) : [];
2025
1747
  const confirmH = modal?.kind === "confirm" ? confirmLines.length + 2 : 0;
2026
1748
  const bodyH = Math.max(8, termRows - (sidebar ? 0 : 1) - bottomH - confirmH);
2027
- const globalFlagged = world.machine.map((m) => [m.name, machineIssues(world, m)]).filter(([, issues]) => issues.length);
2028
- const globalIssues = new Set(globalFlagged.map(([name]) => name)).size;
1749
+ const globalFlagged = machineIssues.filter(([, issues]) => issues.length);
1750
+ const globalIssues = globalFlagged.length;
2029
1751
  const globalProblem = globalFlagged.some(([, issues]) => issues.some((x) => x.severity === "problem"));
2030
- const pluginsWorst = worst([...pluginIssues2.values()].flat().filter((i) => i.severity !== "hint"));
1752
+ const pluginsWorst = worst([...pluginIssues2.values()].flat());
2031
1753
  const badge = (p) => {
2032
- const found = projectIssues(world, p);
2033
- const top = worst(found.map((x) => x.issue));
1754
+ const top = worst(repoIssues2.get(p) ?? repoIssues(world, p));
2034
1755
  return { text: top ? SEV[top.severity].icon : "", width: 3, color: top ? SEV[top.severity].color : void 0 };
2035
1756
  };
2036
1757
  const sideRow = (key, label, here, go, count = []) => ({
@@ -2089,7 +1810,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
2089
1810
  }),
2090
1811
  /* @__PURE__ */ jsx2(Text2, { color: color.accent, children: query ? ` \u2315 ${query}\u258F` : "" })
2091
1812
  ] }) });
2092
- 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;
1813
+ 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;
2093
1814
  const showChips = chips.length > 0;
2094
1815
  const listHeader = /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
2095
1816
  showChips ? chipRow : null,
@@ -2272,9 +1993,9 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
2272
1993
  modal?.kind === "confirm" ? /* @__PURE__ */ jsx2(Panel, { title: modal.fix.label, focused: true, width: columns, height: confirmH, children: confirmLines.map((l, i) => /* @__PURE__ */ jsx2(Text2, { color: color.text, children: l }, i)) }) : null
2273
1994
  ] })
2274
1995
  ] }),
2275
- /* @__PURE__ */ jsxs2(Box2, { height: 1, width: termCols, justifyContent: "space-between", children: [
2276
- /* @__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 }),
2277
- toast ? null : /* @__PURE__ */ jsx2(Text2, { color: color.faint, children: hints + " " })
1996
+ /* @__PURE__ */ jsxs2(Box2, { height: bottomH, width: termCols, justifyContent: "space-between", children: [
1997
+ toastLines.length ? /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: toastLines.map((l, i) => /* @__PURE__ */ jsx2(Text2, { wrap: "truncate-end", color: toast.startsWith("\u2713") ? color.green : toast.startsWith("\u2717") ? color.red : toast.startsWith("!") ? color.yellow : color.muted, children: l }, i)) }) : /* @__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 }),
1998
+ toast && modal?.kind !== "confirm" ? null : /* @__PURE__ */ jsx2(Text2, { color: color.faint, children: " " + hints + " " })
2278
1999
  ] })
2279
2000
  ] });
2280
2001
  }
@@ -2316,25 +2037,23 @@ function Help({ width, height }) {
2316
2037
  ["ctrl+r", "read everything again from disk"],
2317
2038
  ["ctrl+c", "quit"]
2318
2039
  ];
2319
- return /* @__PURE__ */ jsxs2(Panel, { title: "Keys", hint: "any key closes", focused: true, width, height, children: [
2320
- keys.map(([k, d]) => /* @__PURE__ */ jsxs2(Text2, { children: [
2321
- /* @__PURE__ */ jsx2(Text2, { color: color.accent, children: k.padEnd(12) }),
2322
- /* @__PURE__ */ jsx2(Text2, { color: color.text, children: d })
2323
- ] }, k)),
2324
- /* @__PURE__ */ jsx2(Text2, { children: " " }),
2325
- /* @__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' }),
2326
- /* @__PURE__ */ jsx2(Text2, { color: color.muted, children: "Source: lib = from your library \xB7 repo = committed by the team \xB7 untracked = only on this machine" }),
2327
- /* @__PURE__ */ jsx2(Text2, { color: color.muted, children: "Usage counts Claude Code sessions only: other agents don't record skill use." })
2328
- ] });
2040
+ const notes = [
2041
+ '\u2715 problem \xB7 \u26A0 suggestion \xB7 \u2605 recommended fix \xB7 "your call" = a decision, never auto-fixed',
2042
+ "Source: lib = from your library \xB7 repo = committed by the team \xB7 untracked = only on this machine",
2043
+ "Usage counts Claude Code sessions only: skilllib reads no other agent's transcripts."
2044
+ ];
2045
+ const lines = [
2046
+ ...keys.flatMap(([k, d]) => wrap(d, width - 16, 3).map((l, i) => [[(i ? "" : k).padEnd(12), color.accent], [l, color.text]])),
2047
+ [],
2048
+ ...notes.flatMap((n) => wrap(n, width - 4, 3).map((l) => [[l, color.muted]]))
2049
+ ];
2050
+ 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)) });
2329
2051
  }
2330
2052
 
2331
2053
  // src/tui/load.ts
2332
- import { execFileSync, spawn } from "child_process";
2333
- import { existsSync as existsSync3, lstatSync, realpathSync } from "fs";
2334
- import { basename, dirname, join as join2 } from "path";
2335
- function tildify4(p) {
2336
- return tildify(p).replace(/\\/g, "/");
2337
- }
2054
+ import { spawnSync as spawnSync3 } from "child_process";
2055
+ import { existsSync as existsSync3, lstatSync } from "fs";
2056
+ import { basename, dirname as dirname2, join as join2, relative, sep } from "path";
2338
2057
  function uniqueNames(roots) {
2339
2058
  const tail = (r, n) => r.split(/[\\/]/).filter(Boolean).slice(-n).join("/");
2340
2059
  return new Map(
@@ -2345,31 +2064,25 @@ function uniqueNames(roots) {
2345
2064
  })
2346
2065
  );
2347
2066
  }
2348
- function localSkill(root, s, git2) {
2067
+ function localSkill(root, s, git) {
2349
2068
  const managed = {
2350
- ok: {},
2351
- "update available": {},
2352
2069
  "edited locally": { edited: true },
2353
2070
  "edited locally, update available": { edited: true },
2354
- // sync restores it from your library, unless the library doesn't have it (a teammate's skill, another library).
2355
- "folder missing": { missing: true, ...s.latest === null && { library: "missing" } },
2356
- "not in library": { library: "missing" }
2071
+ "folder missing": { missing: true }
2357
2072
  };
2358
2073
  const other = {
2359
- "untracked copy of library skill": { source: "untracked", library: "same" },
2360
- "untracked, differs from library": { source: "untracked", library: "differs" },
2074
+ "untracked copy of library skill": { source: "untracked" },
2075
+ "untracked, differs from library": { source: "untracked" },
2361
2076
  "local only": { source: "untracked" },
2362
2077
  // Installed with `npx skills add` (the repo's skills-lock.json): not skilllib's, so not a library skill.
2363
2078
  "from npx skills": { source: "untracked" },
2364
2079
  "repo skill": { source: "repo" },
2365
- "repo skill, in library": { source: "repo", library: "same" },
2366
- "repo skill, differs from library": { source: "repo", library: "differs" }
2080
+ "repo skill, in library": { source: "repo" },
2081
+ "repo skill, differs from library": { source: "repo" }
2367
2082
  };
2368
- const state = git2 ? git2.of(relativeTo(root, s.path)) : null;
2083
+ const state = git ? git.of(relativeTo(root, s.path)) : null;
2084
+ const others = otherCopies(root, s.name, s.location);
2369
2085
  const own = other[s.state]?.source === "untracked" && (state === "committed" || state === "changed") ? { source: "repo" } : {};
2370
- const blind = s.state === "folder missing" ? [] : s.visibility.filter((v) => v.paths === 0).map((v) => v.id);
2371
- const uncommitted = blind.length ? linkEverywhere(root, s.name, s.location, { dryRun: true }).uncommitted : [];
2372
- const unshared = blind.filter((id) => uncommitted.includes(harness(id).projectDirs[0]));
2373
2086
  return {
2374
2087
  name: s.name,
2375
2088
  source: "lib",
@@ -2377,18 +2090,20 @@ function localSkill(root, s, git2) {
2377
2090
  path: s.path,
2378
2091
  // Agents that actually load it here: visibility lists every agent you use, with `paths: 0` for the blind ones.
2379
2092
  agents: s.visibility.filter((v) => v.paths > 0).map((v) => v.id),
2380
- ...s.managed ? { library: "same", version: s.version ?? void 0, ...managed[s.state] } : { ...other[s.state], ...own },
2381
- ...unshared.length && { unshared },
2093
+ ...s.managed ? { version: s.version ?? void 0, ...managed[s.state] } : { ...other[s.state], ...own },
2382
2094
  ...s.source ? { origin: `npx skills: ${s.source}` } : {},
2383
- added: addedAt(s.path),
2095
+ ...others.length ? { others } : {},
2384
2096
  git: state
2385
2097
  };
2386
2098
  }
2099
+ function otherCopies(root, name, dir) {
2100
+ const self = realPath(join2(root, dir, name));
2101
+ return foldersOf(root, name).real.filter((d) => realPath(join2(d, name)) !== self).map((d) => relativeTo(root, d));
2102
+ }
2387
2103
  function installedAt(path) {
2388
2104
  try {
2389
2105
  const st = lstatSync(path);
2390
- const at = st.birthtimeMs > 0 ? st.birthtime : st.mtime;
2391
- return new Date(at.getTime() - at.getTimezoneOffset() * 6e4).toISOString().slice(0, 16).replace("T", " ");
2106
+ return localStamp((st.birthtimeMs > 0 ? st.birthtime : st.mtime).getTime());
2392
2107
  } catch {
2393
2108
  return void 0;
2394
2109
  }
@@ -2397,8 +2112,8 @@ function machineSkill(s, kept) {
2397
2112
  const global = s.kind === "global" || s.kind === "skills.sh";
2398
2113
  return {
2399
2114
  name: s.name,
2400
- 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",
2401
- 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,
2115
+ source: global ? "global" : s.kind === "plugin" ? "plugin" : s.kind === "claude.ai" ? "claude.ai" : s.kind === "system" || s.kind === "built-in" && s.harnesses[0] === "codex" ? "system" : s.kind === "skilllib" ? "skilllib" : s.kind === "built-in" && s.harnesses[0] === "grok" ? "grok" : "cursor",
2116
+ where: global || s.kind === "skilllib" ? tildify(dirname2(s.path)) : s.kind === "claude.ai" ? "claude.ai account" : s.kind === "built-in" ? `${s.origin} built-in` : s.origin,
2402
2117
  ...s.kind === "skills.sh" ? { origin: `npx skills: ${s.origin}` } : {},
2403
2118
  path: s.path,
2404
2119
  links: s.links,
@@ -2430,28 +2145,17 @@ function pluginOf(p, repoOf) {
2430
2145
  function refOf(p) {
2431
2146
  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 } : {} };
2432
2147
  }
2433
- function dupesOf(r, root, git2 = null) {
2434
- const out = /* @__PURE__ */ new Map();
2435
- 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)) });
2436
- for (const c of r.conflicts)
2437
- 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 })) });
2438
- return out;
2439
- }
2440
2148
  function loadWorld() {
2441
2149
  forgetLatest();
2442
2150
  forgetClaudeBinary();
2443
2151
  const agents = enabledHarnesses();
2444
2152
  const here = projectHere();
2445
- const real = (p) => {
2446
- try {
2447
- return realpathSync(p);
2448
- } catch {
2449
- return p;
2450
- }
2451
- };
2153
+ if (here) rememberProjects([here]);
2154
+ const real = realPath;
2452
2155
  const visible = visibleProjects();
2453
2156
  const cwd = here ? visible.find((p) => real(p) === real(here)) ?? here : null;
2454
- const roots = [.../* @__PURE__ */ new Set([...cwd ? [cwd] : [], ...visible])];
2157
+ const listed = [.../* @__PURE__ */ new Set([...cwd ? [cwd] : [], ...visible])];
2158
+ const roots = readableRoots(listed).readable;
2455
2159
  const names = uniqueNames(roots);
2456
2160
  const descriptions = {};
2457
2161
  const origins = libraryOrigins();
@@ -2459,18 +2163,13 @@ function loadWorld() {
2459
2163
  descriptions[s.name] = s.description;
2460
2164
  return { name: s.name, latest: latestVersion(s.name)?.version ?? 1, origin: origins[s.name] };
2461
2165
  });
2166
+ const libraryNames = new Set(library.map((l) => l.name));
2462
2167
  const kept = keptGlobal();
2463
2168
  const cursorList = agents.includes("cursor") ? cursorPlugins() : [];
2464
- const cursorPlugin = new Map(cursorPluginSkills(cursorList).map((s) => [s.name, s.plugin]));
2465
- const globalDupes = dupesOf(planGlobalTidy({ enabled: agents }), null);
2466
- const sourced = machineSkills(agents);
2169
+ const sourced = machineSkills(agents).filter((s) => s.harnesses.length || s.broken);
2467
2170
  const machine = sourced.map((s) => {
2468
2171
  descriptions[s.name] ??= s.description;
2469
- const m = machineSkill(s, kept);
2470
- if (m.source !== "global" || m.broken) return m;
2471
- const dupes = globalDupes.get(m.name);
2472
- const plugin = cursorPlugin.get(m.name);
2473
- return { ...m, ...dupes ? { dupes } : {}, ...plugin ? { cursorPlugin: plugin } : {} };
2172
+ return machineSkill(s, kept);
2474
2173
  });
2475
2174
  const loadedHere = (root) => {
2476
2175
  const loaded = skillsLoadedIn(root, sourced, agents);
@@ -2478,20 +2177,20 @@ function loadWorld() {
2478
2177
  for (const s of loaded) descriptions[s.name] ??= s.description;
2479
2178
  return { machine: loaded.map((s) => machine[sourced.indexOf(s)] ?? machineSkill(s, kept)) };
2480
2179
  };
2180
+ const statuses = new Map(roots.map((root) => [root, projectStatus(root)]));
2481
2181
  const projects = roots.map((root) => {
2482
- const status = projectStatus(root);
2182
+ const status = statuses.get(root);
2483
2183
  const hasManifest = existsSync3(join2(root, "skilllib.json"));
2484
- const git2 = status.length || hasManifest ? gitInfo(root) : null;
2184
+ const git = status.length || hasManifest ? gitInfo(root) : null;
2485
2185
  for (const s of status) if (!descriptions[s.name] && !s.state.includes("missing")) descriptions[s.name] = readSkillInfo(s.path).description;
2486
- const dupes = status.length ? dupesOf(planProjectTidy(root, { enabled: agents }), root, git2) : /* @__PURE__ */ new Map();
2487
2186
  return {
2488
2187
  name: names.get(root),
2489
2188
  path: root,
2490
- manifest: hasManifest ? git2 ? git2.of("skilllib.json") : "no git" : "none",
2189
+ manifest: hasManifest ? git ? git.of("skilllib.json") : "no git" : "none",
2491
2190
  skills: status.map((s) => {
2492
- const d = dupes.get(s.name);
2493
- const plugin = cursorPlugin.get(s.name);
2494
- return { ...localSkill(root, s, git2), ...d ? { dupes: d } : {}, ...plugin ? { cursorPlugin: plugin } : {} };
2191
+ const local = localSkill(root, s, git);
2192
+ const own = s.state.includes("missing") ? "" : readSkillInfo(s.path).description;
2193
+ return own && own !== descriptions[s.name] ? { ...local, description: own } : local;
2495
2194
  }),
2496
2195
  ...loadedHere(root)
2497
2196
  };
@@ -2501,19 +2200,19 @@ function loadWorld() {
2501
2200
  const plugins = [...installedPlugins(), ...cursorList].map((p) => pluginOf(p, repoOf));
2502
2201
  return {
2503
2202
  agents,
2504
- cwd: cwd ? names.get(cwd) : null,
2203
+ cwd: cwd && names.has(cwd) ? names.get(cwd) : null,
2505
2204
  projects,
2506
2205
  machine,
2507
2206
  plugins,
2508
2207
  library,
2509
2208
  usage: {},
2510
2209
  descriptions,
2511
- backups: backups.map((b) => ({ name: b.name, from: tildify4(b.from), at: b.movedAt })),
2512
- roots: readConfig().roots,
2513
- hidden: readConfig().hidden,
2210
+ backups: backups.map((b) => ({ name: b.name, from: tildify(b.from), at: b.movedAt })),
2211
+ ...(({ roots: roots2, hidden }) => ({ roots: roots2, hidden }))(readConfig()),
2514
2212
  agentsChosen: harnessesChosen(),
2515
2213
  agentSkill: agentSkillState(agents),
2516
- ops: realOps(new Map(projects.map((p) => [p.name, p.path])), backups)
2214
+ issues: findIssues(listed, sourced, libraryNames, (root) => statuses.get(root)),
2215
+ ops: realOps(new Map(projects.map((p) => [p.name, p.path])), backups, sourced, libraryNames)
2517
2216
  };
2518
2217
  }
2519
2218
  async function loadUsage(w) {
@@ -2523,7 +2222,11 @@ async function loadUsage(w) {
2523
2222
  const days = {};
2524
2223
  const midnight = (t) => new Date(t).setHours(0, 0, 0, 0);
2525
2224
  const today = midnight(Date.now());
2225
+ const totals = {};
2526
2226
  for (const use of await scanUsage(30)) {
2227
+ const skill = skillOf(use.skill);
2228
+ totals[skill] = (totals[skill] ?? 0) + 1;
2229
+ if (skill !== use.skill) totals[use.skill] = (totals[use.skill] ?? 0) + 1;
2527
2230
  const root = projectOf(use.cwd);
2528
2231
  const repo = root ? repoOf.get(root) : void 0;
2529
2232
  const day = 29 - Math.round((today - midnight(Date.parse(use.at))) / 864e5);
@@ -2531,38 +2234,29 @@ async function loadUsage(w) {
2531
2234
  (usage[repo] ??= {})[use.skill] = (usage[repo][use.skill] ?? 0) + 1;
2532
2235
  ((days[repo] ??= {})[use.skill] ??= new Array(30).fill(0))[day] += 1;
2533
2236
  }
2534
- return { usage, days };
2535
- }
2536
- function git(root, args) {
2537
- try {
2538
- return execFileSync("git", args, { cwd: root, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
2539
- } catch {
2540
- return "";
2541
- }
2542
- }
2543
- function remoteLabel(url) {
2544
- if (!url) return void 0;
2545
- return url.replace(/^[a-z+]+:\/\//, "").replace(/^[^@/]+@/, "").replace(/:(?!\d)/, "/").replace(/\.git$/, "");
2237
+ return { usage, days, totals };
2546
2238
  }
2547
2239
  function said3(c, repo) {
2548
2240
  if (c.action === "skipped") return `${c.name}: ${c.reason ?? "skipped"}`;
2549
- if (c.action === "removed") return `Removed ${c.name} from ${repo}`;
2241
+ if (c.action === "removed") return `Removed ${c.name} from ${repo}${c.reason ? ` (${c.reason})` : ""}`;
2242
+ if (c.action === "unchanged") return `${c.name}${c.to ? ` v${c.to}` : ""} is already in ${repo}`;
2243
+ if (c.action === "relinked") return `Linked ${c.name}${c.to ? ` v${c.to}` : ""} for your other agents in ${repo}`;
2550
2244
  const back = c.action === "updated" && c.from !== void 0 && c.to !== void 0 && c.to < c.from;
2551
2245
  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}`;
2552
2246
  return c.backedUp ? `${done} (your edits are in Settings \u203A Backups)` : done;
2553
2247
  }
2554
- function realOps(roots, backups) {
2248
+ function realOps(roots, backups, machine, libraryNames) {
2555
2249
  const rootOf = (repo) => roots.get(repo);
2556
2250
  const find = (repo, name) => projectStatus(rootOf(repo)).find((s) => s.name === name);
2557
- const info = /* @__PURE__ */ new Map();
2558
2251
  const result = (c, repo) => c.action === "skipped" ? failed(said3(c, repo)) : said3(c, repo);
2559
2252
  const copyDir = (repo, name) => readManifest(rootOf(repo)).skills[name]?.dir ?? PROJECT_SKILLS_DIR;
2560
2253
  const withLinks = (c, repo, name) => {
2561
2254
  const it = (dirs) => dirs.length === 1 ? "it" : "them";
2255
+ const way = c.inTheWay?.length ? `; not linked everywhere: ${inTheWaySaid(rootOf(repo), c.inTheWay)} (clear the way, then link again)` : "";
2562
2256
  if (c.uncommitted?.length)
2563
- return `${said3(c, repo)}; not linked in ${c.uncommitted.join(", ")}: git tracks ${it(c.uncommitted)} and ${copyDir(repo, name)}/${name} isn't committed, so teammates would get broken links`;
2564
- if (!c.blocked?.length) return result(c, repo);
2565
- return { message: `${said3(c, repo)}; not linked in ${c.blocked.join(", ")}: git tracks ${it(c.blocked)}`, then: allowTracked([{ repo, name, blocked: c.blocked }]) };
2257
+ return `${said3(c, repo)}${way}; not linked in ${c.uncommitted.join(", ")}: git tracks ${it(c.uncommitted)} and ${copyDir(repo, name)}/${name} isn't committed, so teammates would get broken links`;
2258
+ if (!c.blocked?.length) return way ? failed(`${said3(c, repo)}${way}`) : result(c, repo);
2259
+ return { message: `${said3(c, repo)}${way}; not linked in ${c.blocked.join(", ")}: git tracks ${it(c.blocked)}`, then: allowTracked([{ repo, name, blocked: c.blocked }]) };
2566
2260
  };
2567
2261
  const addTo = (repos, names) => {
2568
2262
  if (repos.length === 1 && names.length === 1) return withLinks(addSkill(rootOf(repos[0]), names[0]), repos[0], names[0]);
@@ -2584,10 +2278,11 @@ function realOps(roots, backups) {
2584
2278
  const why = kind === "blocked" ? `git tracks ${dirs.length === 1 ? "it" : "them"}` : `${copies.join(", ")} ${copies.length === 1 ? "isn't" : "aren't"} committed, so a link would break for teammates`;
2585
2279
  notes.push(`${who}not linked in ${dirs.join(", ")} in ${repo}: ${why}`);
2586
2280
  }
2281
+ for (const x of done.filter((d) => d.c.inTheWay?.length)) notes.push(`${x.name} not linked everywhere in ${x.repo}: ${inTheWaySaid(rootOf(x.repo), x.c.inTheWay)}`);
2587
2282
  const skipped = changes.filter((x) => x.c.action === "skipped").map((x) => `${x.name} not added to ${x.repo}: ${x.c.reason}`);
2588
2283
  const text = [summary, ...notes, ...skipped].filter(Boolean).join("; ");
2589
2284
  const held = done.flatMap((x) => x.c.blocked?.length ? [{ repo: x.repo, name: x.name, blocked: x.c.blocked }] : []);
2590
- const result2 = !done.length ? failed(text) : held.length ? { message: text, then: allowTracked(held) } : skipped.length ? failed(text) : text;
2285
+ const result2 = !done.length ? failed(text) : held.length ? { message: text, then: allowTracked(held) } : skipped.length || done.some((x) => x.c.inTheWay?.length) ? failed(text) : text;
2591
2286
  return { result: result2, skipped, notes };
2592
2287
  };
2593
2288
  const allowTracked = (held) => {
@@ -2596,14 +2291,13 @@ function realOps(roots, backups) {
2596
2291
  return {
2597
2292
  label: `Also link ${new Set(held.map((h) => h.name)).size === 1 ? "it" : "them"} in ${dirs.join(", ")}`,
2598
2293
  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(", ")}.`,
2599
- run: (w) => {
2294
+ run: () => {
2600
2295
  for (const repo of repos) allowTrackedLinks(rootOf(repo));
2601
- if (held.length === 1) return w.ops.link(held[0].repo, held[0].name, true);
2602
2296
  const results = held.map((h) => ({ ...h, r: linkIn(h.repo, h.name, true) }));
2603
2297
  const linked = results.filter((x) => x.r.created.length);
2604
2298
  const names = [...new Set(linked.map((x) => x.name))];
2605
2299
  const summary = linked.length ? `Linked ${names.length === 1 ? names[0] : `${names.length} skills`} in ${[...new Set(linked.flatMap((x) => x.r.created))].join(", ")} in ${[...new Set(linked.map((x) => x.repo))].join(", ")}` : "";
2606
- const others = results.filter((x) => !x.r.created.length || x.r.uncommitted.length).map((x) => `${x.repo}: ${linkSaid(x.name, x.r)}`);
2300
+ const others = results.filter((x) => !x.r.created.length || x.r.uncommitted.length || x.r.inTheWay.length).map((x) => `${x.repo}: ${linkSaid(x.repo, x.name, x.r)}`);
2607
2301
  return [summary, ...others].join("; ");
2608
2302
  }
2609
2303
  };
@@ -2612,15 +2306,19 @@ function realOps(roots, backups) {
2612
2306
  const s = find(repo, name);
2613
2307
  return s.managed ? relinkDependency(rootOf(repo), name, { allowTracked: allow }) : linkEverywhere(rootOf(repo), name, s.location, { allowTracked: allow });
2614
2308
  };
2615
- const linkSaid = (name, r) => {
2309
+ const linkSaid = (repo, name, r) => {
2616
2310
  const linked = r.created.length ? `linked in ${r.created.join(", ")}` : "";
2617
2311
  const notLinked = r.uncommitted.length ? `not linked in ${r.uncommitted.join(", ")}: git tracks ${r.uncommitted.length === 1 ? "it" : "them"} and ${name} isn't committed, so teammates would get broken links` : "";
2618
- return `${name}: ${[linked, notLinked].filter(Boolean).join("; ") || "nothing to link"}`;
2312
+ const inTheWay = r.inTheWay.length ? inTheWaySaid(rootOf(repo), r.inTheWay) : "";
2313
+ return `${name}: ${[linked, notLinked, inTheWay].filter(Boolean).join("; ") || "nothing to link"}`;
2619
2314
  };
2620
- const orForce = (c, repo, label, force) => c.action === "skipped" && c.reason?.includes("edits") ? {
2621
- message: `${c.name} in ${repo} has local edits`,
2622
- then: { label, preview: `Your edits to ${c.name} in ${repo} go to Settings \u203A Backups.`, run: () => result(force(), repo) }
2623
- } : result(c, repo);
2315
+ const orForce = (c, repo, label, force) => (
2316
+ // Whatever --force would get past (edits, a pin from another library, another copy in the way): offer it here.
2317
+ c.action === "skipped" && !!c.reason?.includes("--force") ? {
2318
+ message: `${c.name} in ${repo}: ${c.reason.replace(/ \(use --force[^)]*\)$/, "")}`,
2319
+ then: { label, preview: `What's there now in ${repo} goes to Settings \u203A Backups first.`, run: () => result(force(), repo) }
2320
+ } : result(c, repo)
2321
+ );
2624
2322
  const toLibrary = (path) => {
2625
2323
  const r = importSkill(path, { force: true });
2626
2324
  return r.status === "unchanged" ? `${r.name} is already the same in your library` : `${r.name} v${latestVersion(r.name)?.version} is in your library`;
@@ -2633,130 +2331,92 @@ function realOps(roots, backups) {
2633
2331
  if (!s) return failed(`${name} isn't in ${repo}`);
2634
2332
  const committed = !s.managed && ["committed", "changed"].includes(gitInfo(rootOf(repo))?.of(relativeTo(rootOf(repo), s.path)) ?? "");
2635
2333
  if (s.state.startsWith("repo skill") || committed) return failed(`${name} is the repo's own (committed by your team): skilllib doesn't delete it`);
2636
- if (!s.managed) return removeUntracked(rootOf(repo), name, s.path), `Removed ${name} from ${repo} (in Settings \u203A Backups)`;
2637
- return orForce(removeSkill(rootOf(repo), name), repo, "Remove it anyway", () => removeSkill(rootOf(repo), name, { force: true }));
2638
- },
2639
- restore: (repo, name) => {
2640
- const [c] = syncSkill(rootOf(repo), name);
2641
- return c ? result(c, repo) : `${name} is already in place`;
2334
+ const others = otherCopies(rootOf(repo), name, s.location);
2335
+ const left = (r) => typeof r === "string" && others.length ? `${r}; the copy in ${others.join(", ")} stays` : r;
2336
+ if (!s.managed) return removeUntracked(rootOf(repo), name, s.path), left(`Removed ${others.length ? `${s.location}/${name}` : name} from ${repo} (in Settings \u203A Backups)`);
2337
+ return left(orForce(removeSkill(rootOf(repo), name), repo, "Remove it anyway", () => removeSkill(rootOf(repo), name, { force: true })));
2642
2338
  },
2643
2339
  update: (repo, name) => {
2644
2340
  const c = updateProject(rootOf(repo), [name])[0];
2645
2341
  return c ? orForce(c, repo, "Update anyway", () => updateProject(rootOf(repo), [name], { force: true })[0]) : `${name} is up to date`;
2646
2342
  },
2647
- saveEdits: (repo, name) => {
2648
- const s = find(repo, name);
2649
- const saved = toLibrary(s.path);
2650
- addSkill(rootOf(repo), name);
2651
- return saved;
2652
- },
2653
- discardEdits: (repo, name) => {
2654
- const s = find(repo, name);
2655
- removeUntracked(rootOf(repo), name, s.path);
2656
- return withLinks(addSkill(rootOf(repo), name, s.managed && s.version ? { version: s.version } : {}), repo, name);
2657
- },
2658
- link: (repo, name, allow = false) => {
2659
- const r = linkIn(repo, name, allow);
2660
- const message = linkSaid(name, r);
2661
- if (!r.blocked.length || allow) return r.created.length ? message : failed(message);
2662
- return { message: `${message}; not linked in ${r.blocked.join(", ")}: git tracks ${r.blocked.length === 1 ? "it" : "them"}`, then: allowTracked([{ repo, name, blocked: r.blocked }]) };
2663
- },
2664
- track: (repo, name) => withLinks(addSkill(rootOf(repo), name), repo, name),
2665
- importLocal: (repo, name) => {
2666
- const s = find(repo, name);
2667
- importSkill(s.path);
2668
- return withLinks(addSkill(rootOf(repo), name), repo, name);
2669
- },
2670
2343
  copyToLibrary: (repo, name) => toLibrary(find(repo, name).path),
2671
- tidy: (repo, name, { keep, allowGit = false } = {}) => {
2672
- const root = repo === null ? null : rootOf(repo);
2673
- const opts = keep ? { keep: { [name]: keep } } : {};
2674
- const plan = (root ? planProjectTidy(root, opts) : planGlobalTidy(opts)).plans.find((p) => p.name === name);
2675
- if (!plan) return `${name}: nothing to tidy`;
2676
- const r = applyTidy(plan, { git: allowGit ? "go" : "keep" });
2677
- const steps = (list) => list.map((s) => describeStep(s, root)).join(", ");
2678
- if (!r.held.length) return r.applied.length ? `${name}: ${steps(r.applied)}` : `${name}: nothing changed`;
2679
- return {
2680
- message: `${name}: ${r.applied.length ? `${steps(r.applied)}; ` : ""}held back ${steps(r.held)}, because git tracks it`,
2681
- then: {
2682
- label: "Also change what git tracks",
2683
- preview: `${steps(r.held)}: these changes will show in git status in ${repo}. Replaced copies go to Settings \u203A Backups.`,
2684
- run: (w) => w.ops.tidy(repo, name, { keep, allowGit: true })
2685
- }
2686
- };
2687
- },
2688
- unloadGlobal: (m) => {
2689
- if (!m.broken && !latestVersion(m.name)) importSkill(m.path);
2690
- const r = unloadGlobal(m.path, m.links);
2691
- return r.ok ? `${m.name} no longer loads globally (in Settings \u203A Backups)` : failed(`${m.name}: ${r.reason}`);
2692
- },
2693
2344
  deleteGlobal: (m) => {
2694
- const r = deleteGlobal(m.path, m.links);
2695
- return r.ok ? m.broken ? `Removed the broken link ${m.name}` : `Deleted ${m.name} (in Settings \u203A Backups)` : failed(`${m.name}: ${r.reason}`);
2345
+ if (!entryExists(m.path)) return `${tildify(m.path)} is already gone`;
2346
+ const others = machine.filter((o) => o.name === m.name && o.path !== m.path && o.kind !== "plugin" && !o.broken);
2347
+ const dependents = others.filter((o) => !survives(o.path, [m.path, ...m.links]));
2348
+ const stays = others.filter((o) => !dependents.includes(o));
2349
+ if (!m.broken) {
2350
+ const readers = [.../* @__PURE__ */ new Set([...m.agents, ...dependents.flatMap((o) => o.harnesses)])].filter((id) => !stays.some((o) => o.harnesses.includes(id)));
2351
+ const blind = blindWithoutGlobal(m.name, [{ path: m.path, harnesses: readers, links: [...m.links, ...dependents.map((o) => o.path)] }], [...roots.values(), ...knownProjects()]);
2352
+ if (blind.length) return failed(`${m.name} kept: ${blindSaid(m.name, blind, (root) => [...roots].find(([, r2]) => r2 === root)?.[0] ?? basename(root))}`);
2353
+ }
2354
+ const left = m.broken ? [] : strandedLinks(m.name, [{ path: m.path, links: [...m.links, ...dependents.map((o) => o.path)] }], [...roots.values(), ...knownProjects()], (root) => [...roots].find(([, r2]) => r2 === root)?.[0] ?? basename(root));
2355
+ const r = deleteGlobal(m.path, [...m.links, ...dependents.map((o) => o.path)]);
2356
+ if (!r.ok) return failed(`${m.name}: ${r.reason}`);
2357
+ if (m.broken) return `Removed the broken link ${m.name}`;
2358
+ const still = stays.map((o) => tildify(o.path));
2359
+ return `Deleted ${tildify(m.path)} (in Settings \u203A Backups)${still.length ? `; ${m.name} still loads globally from ${still.join(", ")}` : ""}${left.length ? `; ${left.join(", ")} now point${left.length === 1 ? "s" : ""} at nothing: remove ${left.length === 1 ? "it" : "them"}` : ""}`;
2696
2360
  },
2697
2361
  keepGlobal: (name, keep) => (setKeepGlobal([name], keep), keep ? `${name} marked as global on purpose` : `${name} is no longer marked as global on purpose`),
2698
- linkGlobal: (m, agents) => {
2699
- const r = linkGlobal(m.path, agents);
2700
- const linked = r.linked.map(tildify4).join(", ");
2701
- const skipped = r.skipped.length ? `; ${r.skipped.map(tildify4).join(", ")} already holds a different skill` : "";
2702
- const message = `${m.name}: ${linked ? `linked ${linked}` : "nothing linked"}${skipped}`;
2703
- return !linked && skipped ? failed(message) : message;
2704
- },
2705
2362
  moveGlobal: (skills, repos, group) => {
2706
- const moved = [];
2707
- const notes = [];
2708
- const held = [];
2709
2363
  const repoRoots = repos.map(rootOf);
2710
- for (const mv of moveGlobalSkills(skills, repoRoots, { group, label: (root) => repos[repoRoots.indexOf(root)] })) {
2711
- if (!mv.moved) {
2712
- notes.push(`${mv.name}: ${mv.reason}`);
2713
- continue;
2714
- }
2715
- moved.push(mv.name);
2716
- for (const [i, [root, c]] of mv.changes.entries()) {
2717
- const repo = repos[i];
2718
- const blind = visibilityOf(root, mv.name).flatMap((v) => v.paths === 0 ? [v.id] : []);
2719
- const blocked = [...c.blocked ?? [], ...c.uncommitted ?? []];
2720
- if (!blocked.length || !blind.length) continue;
2721
- held.push({ repo, name: mv.name, blocked, blind, copy: copyDir(repo, mv.name), shared: !c.uncommitted?.length });
2722
- }
2364
+ const label = (root) => repos[repoRoots.indexOf(root)] ?? [...roots].find(([, r]) => r === root)?.[0] ?? basename(root);
2365
+ const picked = new Set(skills.map((m) => m.name));
2366
+ const sourced = machine.filter((o) => picked.has(o.name) && o.movable && !o.broken);
2367
+ const attempt = (names) => moveGlobalSkills(names ? sourced.filter((m) => names.includes(m.name)) : sourced, repoRoots, { group, label, allowTrackedHint: "allow links there, then move it again" });
2368
+ const said4 = (results2) => {
2369
+ const moved2 = results2.filter((mv) => mv.moved).map((mv) => mv.name);
2370
+ const which = moved2.length <= 4 ? moved2.join(", ") : `${moved2.length} skills`;
2371
+ const summary = moved2.length ? `${which} now load${moved2.length === 1 ? "s" : ""} only in ${repos.join(", ")}` : "";
2372
+ const left = results2.flatMap((mv) => mv.left ?? []);
2373
+ return {
2374
+ moved: moved2,
2375
+ text: [summary, ...left.length ? [`${left.join(", ")} now point${left.length === 1 ? "s" : ""} at nothing: remove ${left.length === 1 ? "it" : "them"}`] : [], ...results2.filter((mv) => !mv.moved).map((mv) => `${mv.name}: ${mv.reason}`)].filter(Boolean).join("; ") || "Nothing moved"
2376
+ };
2377
+ };
2378
+ const results = attempt();
2379
+ const { moved, text } = said4(results);
2380
+ const finishable = (mv) => !mv.moved && mv.changes.length > 0 && mv.changes.every(([, c]) => c.action !== "skipped" && !c.inTheWay?.length && !c.uncommitted?.length) && mv.changes.some(([, c]) => c.blocked?.length);
2381
+ const held = results.filter(finishable).flatMap((mv) => mv.changes.flatMap(([root, c]) => c.blocked?.length ? [{ repo: label(root), name: mv.name, blocked: c.blocked }] : []));
2382
+ if (held.length) {
2383
+ const ask = allowTracked(held);
2384
+ const names = [...new Set(held.map((h) => h.name))];
2385
+ return {
2386
+ message: text,
2387
+ then: {
2388
+ ...ask,
2389
+ label: `${ask.label}, then stop loading ${names.length === 1 ? "it" : "them"} globally`,
2390
+ // The retry is what says whether it's done: a skill still global after it is a failure, said as one.
2391
+ run: (w) => {
2392
+ const linked = said(ask.run(w));
2393
+ const retry = said4(attempt(names));
2394
+ const before = said4(results.filter((mv) => mv.moved));
2395
+ const others = results.filter((mv) => !mv.moved && !names.includes(mv.name)).map((mv) => `${mv.name}: ${mv.reason}`);
2396
+ const text2 = [before.moved.length ? before.text : "", linked, retry.text, ...others].filter(Boolean).join("; ");
2397
+ return retry.moved.length === names.length ? text2 : failed(text2);
2398
+ }
2399
+ }
2400
+ };
2723
2401
  }
2724
- const gaps = /* @__PURE__ */ new Map();
2725
- for (const repo of new Set(held.map((h) => h.repo)))
2726
- for (const shared of [true, false]) {
2727
- const here = held.filter((h) => h.repo === repo && h.shared === shared);
2728
- if (!here.length) continue;
2729
- const agents = [...new Set(here.flatMap((h) => h.blind))].map((id) => harness(id).name).join(", ");
2730
- const what = skills.length === 1 ? "it" : here.length === moved.length ? "them" : here.map((h) => h.name).join(", ");
2731
- const dirs = [...new Set(here.flatMap((h) => h.blocked))].join(", ");
2732
- const why = shared ? `git tracks ${dirs}` : `${[...new Set(here.map((h) => h.copy))].join(", ")} isn't committed, so a link in ${dirs} would break for teammates`;
2733
- const key = `${agents}|${what}|${why}`;
2734
- if (!gaps.has(key)) gaps.set(key, { who: `${agents} can't see ${what}`, why, repos: [] });
2735
- gaps.get(key).repos.push(repo);
2736
- }
2737
- const blindNotes = [...gaps.values()].map((g) => `${g.who} in ${g.repos.join(", ")}: ${g.why}`);
2738
- const summary = moved.length ? `${moved.length === 1 ? moved[0] : `${moved.length} skills`} now load${moved.length === 1 ? "s" : ""} only in ${repos.join(", ")}` : "";
2739
- const text = [summary, ...blindNotes, ...notes].filter(Boolean).join("; ") || "Nothing moved";
2740
- const askable = held.filter((h) => h.shared);
2741
- if (!moved.length && notes.length) return failed(text);
2742
- return askable.length ? { message: text, then: allowTracked(askable) } : text;
2402
+ return moved.length ? text : failed(text);
2743
2403
  },
2744
2404
  createSkill: (name) => {
2745
2405
  const r = createSkill(name, "");
2746
2406
  return r.ok ? `Created ${name} in your library: edit its SKILL.md from Your skills` : failed(`${name}: ${r.reason}`);
2747
2407
  },
2748
- pluginOffHere: (repo, id) => {
2749
- const root = rootOf(repo);
2750
- const plugin = claudePlugins(skillsLoadedIn(root, machineSkills()), root).find((p) => p.id === id);
2751
- if (!plugin) return failed(`${id} isn't on in ${repo}`);
2752
- const r = turnOffIn(plugin, root);
2753
- return r.ok ? r.message : failed(r.message);
2754
- },
2755
2408
  replacePlugin: (p, repos) => {
2756
2409
  const id = p.id;
2410
+ const clashes = p.skills.filter((s) => {
2411
+ const mine = treeHash(librarySkillDir(s.name));
2412
+ return mine !== null && mine !== treeHash(s.path);
2413
+ });
2414
+ 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`);
2415
+ const cant = repos.flatMap(
2416
+ (repo) => p.skills.flatMap((s) => ((why) => why ? [`${s.name} in ${repo}: ${why.replace(/ \(use --force[^)]*\)$/, "")}`] : [])(trackBlocker(rootOf(repo), s.name) ?? addRefusal(rootOf(repo), s.name, treeHash(s.path) ?? "")))
2417
+ );
2418
+ if (cant.length) return failed(`${id} kept, nothing changed: ${cant.join("; ")}`);
2757
2419
  const imported = p.skills.map((s) => ({ name: s.name, status: importSkill(s.path).status }));
2758
- const clashes = imported.filter((r) => r.status === "exists").map((r) => r.name);
2759
- if (clashes.length) return failed(`${id} kept: your library has different ${clashes.join(", ")}; update or delete ${clashes.length === 1 ? "it" : "them"} first`);
2760
2420
  for (const r of imported) if (r.status !== "unchanged") recordOrigin(r.name, `plugin: ${id}`);
2761
2421
  const added = repos.length ? addMany(repos, p.skills.map((s) => s.name)) : { result: "", skipped: [], notes: [] };
2762
2422
  if (added.skipped.length) return failed(`${id} kept: ${added.skipped.join("; ")}`);
@@ -2792,21 +2452,31 @@ function realOps(roots, backups) {
2792
2452
  return r.ok ? r.message : failed(r.message);
2793
2453
  },
2794
2454
  deleteLibrary: (name) => {
2455
+ const using = projectsUsing(name);
2456
+ const unreadable = readableRoots(using).unreadable.map((u) => u.root);
2457
+ if (unreadable.length) return failed(`${name} is pinned in ${unreadable.map((r2) => basename(r2)).join(", ")}, whose skilllib.json can't be read: fix it first, then delete`);
2458
+ const hidden = using.filter((root) => ![...roots.values()].includes(root));
2459
+ if (hidden.length) return failed(`${name} is also in ${hidden.map((r2) => basename(r2)).join(", ")}, which ${hidden.length === 1 ? "is" : "are"} hidden: unhide ${hidden.length === 1 ? "it" : "them"} (Settings) to delete it everywhere`);
2795
2460
  const from = [];
2796
- for (const root of projectsUsing(name)) {
2461
+ const left = [];
2462
+ for (const root of using) {
2797
2463
  const s = projectStatus(root).find((x) => x.name === name && x.managed);
2798
- if (s && existsSync3(s.path)) removeUntracked(root, name, s.path);
2799
- removeSkill(root, name, { force: true });
2800
- from.push([...roots].find(([, r2]) => r2 === root)?.[0] ?? basename(root));
2464
+ if (s && isDir(s.path) && !notWritable(root, relative(root, dirname2(s.path)).split(sep).join("/"))) removeUntracked(root, name, s.path);
2465
+ const c = removeSkill(root, name, { force: true });
2466
+ if (c.action === "skipped") return failed(`${name}: ${c.reason}${from.length ? `; removed from ${from.join(", ")} so far` : ""}`);
2467
+ const repo = [...roots].find(([, r2]) => r2 === root)?.[0] ?? basename(root);
2468
+ from.push(repo);
2469
+ if (c.reason) left.push(`in ${repo}, ${c.reason}`);
2801
2470
  }
2802
2471
  const r = deleteLibrarySkill(name);
2803
2472
  if (!r.ok) return failed(`${name}: ${r.reason}`);
2804
- return `Deleted ${name} from your library${from.length ? ` and from ${from.join(", ")}` : ""} (in Settings \u203A Backups)`;
2473
+ return `Deleted ${name} from your library${from.length ? ` and from ${from.join(", ")}` : ""} (in Settings \u203A Backups)${left.length ? `; ${left.join("; ")}` : ""}`;
2805
2474
  },
2806
2475
  restoreBackup: (i) => {
2807
- const b = backups[i];
2808
- const r = b.kind === "plugin" ? restorePlugin(b) : restoreBackup(b);
2809
- return r.ok ? `Restored ${b.name} to ${tildify4(r.to)}` : failed(`${b.name}: ${r.reason}`);
2476
+ const done = movedTogether(backups, backups[i]).map((b) => [b, b.kind === "plugin" ? restorePlugin(b) : restoreBackup(b)]);
2477
+ const bad = done.filter(([, r]) => !r.ok);
2478
+ if (bad.length) return failed(bad.map(([b, r]) => `${b.name}: ${!r.ok ? r.reason : ""}`).join("; "));
2479
+ return done.map(([b, r]) => `Restored ${b.name} to ${r.ok ? tildify(r.to) : ""}`).join(" \xB7 ");
2810
2480
  },
2811
2481
  setAgents: (ids) => (setHarnesses(ids), "Saved your agents"),
2812
2482
  installVersion: (repo, name, version) => withLinks(addSkill(rootOf(repo), name, { version, force: true }), repo, name),
@@ -2815,40 +2485,37 @@ function realOps(roots, backups) {
2815
2485
  addRoot: (path) => {
2816
2486
  const root = addRoot(path);
2817
2487
  discoverProjects();
2818
- return `Scanning ${tildify4(root)} for repos`;
2488
+ return `Scanning ${tildify(root)} for repos`;
2819
2489
  },
2820
- removeRoot: (path) => (removeRoot(path), `Stopped scanning ${tildify4(path)}; repos already found stay until you hide them`),
2490
+ removeRoot: (path) => (removeRoot(path), `Stopped scanning ${tildify(path)}; repos already found stay until you hide them`),
2821
2491
  rescan: () => (discoverProjects(), "Looked for repos in your project folders again"),
2822
2492
  hide: (repo) => (setHidden(rootOf(repo), true), `Hid ${repo}: Settings \u203A Hidden repos brings it back`),
2823
- unhide: (path) => (setHidden(path, false), `${tildify4(path)} is back in the list`),
2493
+ unhide: (path) => (setHidden(path, false), `${tildify(path)} is back in the list`),
2824
2494
  openFolder: (path) => {
2825
2495
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
2826
- spawn(cmd, [path], { detached: true, stdio: "ignore" }).unref();
2827
- return `Opened ${tildify4(path)}`;
2496
+ const r = spawnSync3(cmd, [path], { stdio: "ignore", timeout: 1e4 });
2497
+ return r.error || r.status !== 0 && process.platform !== "win32" ? failed(`Couldn't open ${tildify(path)} (${cmd} ${r.error ? "isn't available" : "failed"})`) : `Opened ${tildify(path)}`;
2828
2498
  },
2829
2499
  copy: (text, what) => {
2830
2500
  const r = copyText(text);
2831
- 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)}`;
2501
+ return r.copied ? `Copied ${what}: paste it into Claude Code, Cursor, Codex or Grok` : `Couldn't reach the clipboard; ${what} is in ${tildify(r.savedTo)}`;
2832
2502
  },
2833
2503
  installAgentSkill: () => {
2834
2504
  const r = installAgentSkill();
2835
- return r.ok ? `Your agents can now use skilllib (${r.dirs.map(tildify4).join(", ")})` : failed(`skilllib skill: ${r.reason}`);
2505
+ return r.ok ? `Your agents can now use skilllib (${r.dirs.map(tildify).join(", ")})` : failed(`skilllib skill: ${r.reason}`);
2836
2506
  },
2837
2507
  removeAgentSkill: () => {
2838
2508
  const removed = removeAgentSkill();
2839
- return removed.length ? `Removed the skilllib skill (${removed.map(tildify4).join(", ")})` : failed("The skilllib skill isn't installed");
2509
+ return removed.length ? `Removed the skilllib skill (${removed.map(tildify).join(", ")})` : failed("The skilllib skill isn't installed");
2840
2510
  },
2841
- repoInfo: (repo) => {
2842
- if (!info.has(repo)) {
2843
- const root = rootOf(repo);
2844
- const status = git(root, ["status", "--porcelain"]);
2845
- info.set(repo, {
2846
- remote: remoteLabel(git(root, ["remote", "get-url", "origin"])),
2847
- branch: git(root, ["rev-parse", "--abbrev-ref", "HEAD"]) || void 0,
2848
- dirty: status ? status.split("\n").length : 0
2849
- });
2850
- }
2851
- return info.get(repo);
2511
+ usageIssues: ({ usage, totals = {} }) => {
2512
+ const repoOf = new Map([...roots].map(([name, root]) => [root, name]));
2513
+ return usageIssues([...roots.values()], machine, libraryNames, {
2514
+ inProject: (root, skill) => usage[repoOf.get(root)]?.[skill] ?? 0,
2515
+ total: (skill) => totals[skill] ?? 0,
2516
+ asUsed: (key) => totals[key] ?? 0,
2517
+ days: 30
2518
+ });
2852
2519
  }
2853
2520
  };
2854
2521
  }
@@ -2856,6 +2523,7 @@ function realOps(roots, backups) {
2856
2523
  // src/tui/index.tsx
2857
2524
  import { jsx as jsx3 } from "react/jsx-runtime";
2858
2525
  async function tui() {
2526
+ discoverProjects();
2859
2527
  const instance = render(/* @__PURE__ */ jsx3(App, { initial: loadWorld(), reload: loadWorld, loadUsage }), { alternateScreen: true, exitOnCtrlC: true });
2860
2528
  await instance.waitUntilExit();
2861
2529
  }