skilllib 1.6.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -38
- package/dist/{chunk-PHRUPZGX.js → chunk-Z2F5W3DY.js} +852 -325
- package/dist/index.js +9 -8
- package/dist/json.d.cts +200 -0
- package/dist/json.d.ts +200 -0
- package/dist/{tui-WYVSDGHD.js → tui-7CMGG6HX.js} +715 -235
- package/package.json +16 -3
- package/skills/skilllib/SKILL.md +5 -4
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
HARNESSES,
|
|
3
|
+
PROJECT_SKILLS_DIR,
|
|
3
4
|
addRoot,
|
|
4
5
|
addSkill,
|
|
6
|
+
addedAt,
|
|
7
|
+
agentSkillState,
|
|
5
8
|
allowTrackedLinks,
|
|
6
9
|
applyTidy,
|
|
7
10
|
claudeBinary,
|
|
@@ -9,19 +12,21 @@ import {
|
|
|
9
12
|
copyLabel,
|
|
10
13
|
createSkill,
|
|
11
14
|
cursorPluginSkills,
|
|
15
|
+
cursorPlugins,
|
|
12
16
|
deleteGlobal,
|
|
13
17
|
deleteLibrarySkill,
|
|
14
18
|
describeStep,
|
|
15
19
|
discoverProjects,
|
|
16
20
|
enabledHarnesses,
|
|
17
|
-
|
|
21
|
+
forgetClaudeBinary,
|
|
18
22
|
forgetLatest,
|
|
19
23
|
gitInfo,
|
|
20
24
|
gitVisibleSteps,
|
|
21
25
|
harness,
|
|
22
26
|
harnessesChosen,
|
|
23
27
|
importSkill,
|
|
24
|
-
|
|
28
|
+
installAgentSkill,
|
|
29
|
+
installedPlugins,
|
|
25
30
|
keptGlobal,
|
|
26
31
|
latestVersion,
|
|
27
32
|
libraryOrigins,
|
|
@@ -34,15 +39,17 @@ import {
|
|
|
34
39
|
planGlobalTidy,
|
|
35
40
|
planProjectTidy,
|
|
36
41
|
pluginBackups,
|
|
42
|
+
projectHere,
|
|
37
43
|
projectOfFactory,
|
|
38
44
|
projectStatus,
|
|
39
45
|
projectsUsing,
|
|
40
46
|
readConfig,
|
|
47
|
+
readManifest,
|
|
41
48
|
readSkillInfo,
|
|
42
49
|
recordOrigin,
|
|
43
|
-
recordRemovedPlugin,
|
|
44
50
|
relativeTo,
|
|
45
51
|
relinkDependency,
|
|
52
|
+
removeAgentSkill,
|
|
46
53
|
removeRoot,
|
|
47
54
|
removeSkill,
|
|
48
55
|
removeUntracked,
|
|
@@ -52,17 +59,20 @@ import {
|
|
|
52
59
|
setHarnesses,
|
|
53
60
|
setHidden,
|
|
54
61
|
setKeepGlobal,
|
|
55
|
-
|
|
62
|
+
setPluginOn,
|
|
56
63
|
skilllibHome,
|
|
57
64
|
skillsLoadedIn,
|
|
58
65
|
syncProject,
|
|
66
|
+
tildify,
|
|
59
67
|
turnOffIn,
|
|
68
|
+
uninstallPlugin,
|
|
60
69
|
unloadGlobal,
|
|
70
|
+
updatePlugin,
|
|
61
71
|
updateProject,
|
|
62
|
-
userHome,
|
|
63
72
|
versionHistory,
|
|
73
|
+
visibilityOf,
|
|
64
74
|
visibleProjects
|
|
65
|
-
} from "./chunk-
|
|
75
|
+
} from "./chunk-Z2F5W3DY.js";
|
|
66
76
|
|
|
67
77
|
// src/tui/index.tsx
|
|
68
78
|
import { render } from "ink";
|
|
@@ -188,10 +198,7 @@ import { spawnSync } from "child_process";
|
|
|
188
198
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
189
199
|
import { join } from "path";
|
|
190
200
|
var EXCERPT_CHARS = 1200;
|
|
191
|
-
|
|
192
|
-
const home = userHome();
|
|
193
|
-
return p.startsWith(home) ? "~" + p.slice(home.length) : p;
|
|
194
|
-
}
|
|
201
|
+
var tildify2 = (p) => tildify(p);
|
|
195
202
|
function skillExcerpt(dir, chars) {
|
|
196
203
|
const file = join(dir, "SKILL.md");
|
|
197
204
|
if (!existsSync(file)) return { text: "(SKILL.md not found)", files: 0, size: 0 };
|
|
@@ -215,7 +222,7 @@ function buildReviewPrompt(skills, ctx) {
|
|
|
215
222
|
`- Description: ${s.description || "(none)"}`,
|
|
216
223
|
`- Source: ${s.source}`,
|
|
217
224
|
`- Installed via: ${s.installedHow}`,
|
|
218
|
-
`- Location: ${
|
|
225
|
+
`- Location: ${tildify2(s.path)}${s.links.length ? ` (also linked from ${s.links.map(tildify2).join(", ")})` : ""}`,
|
|
219
226
|
`- Loaded by: ${s.loadedBy}`,
|
|
220
227
|
...s.vendor ? [`- Vendor-managed: ${s.vendor}`] : [],
|
|
221
228
|
...s.keptGlobal ? ["- I marked this as global on purpose: recommend changing that only for a strong reason (unused, duplicated, low quality)"] : [],
|
|
@@ -237,11 +244,11 @@ function buildReviewPrompt(skills, ctx) {
|
|
|
237
244
|
- A skill is a folder with a SKILL.md. Every loaded skill's name and description sit in the agent's context in every session, so each one costs context and can trigger when it shouldn't.
|
|
238
245
|
- "Global" skills load in EVERY repo for the listed agents. Project skills only load inside that repo.
|
|
239
246
|
- I use these agents: ${ctx.harnesses.join(", ") || "(none configured)"}.
|
|
240
|
-
- I manage skills with skilllib, which can: keep a skill global; move it into specific projects (and out of global); delete it (a backup is kept); or leave vendor skills alone. Vendor skills (plugins, claude.ai, Cursor built-ins) can only be turned off at their source.
|
|
247
|
+
- I manage skills with skilllib, which can: keep a skill global; move it into specific projects (and out of global); delete it (a backup is kept); or leave vendor skills alone. Vendor skills (plugins, claude.ai, Cursor built-ins, Grok bundled skills) can only be turned off at their source.
|
|
241
248
|
- 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.
|
|
242
249
|
|
|
243
250
|
## My projects
|
|
244
|
-
${ctx.projects.map((p) => `- ${p.name} \u2014 ${
|
|
251
|
+
${ctx.projects.map((p) => `- ${p.name} \u2014 ${tildify2(p.path)}`).join("\n") || "(none)"}
|
|
245
252
|
|
|
246
253
|
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).
|
|
247
254
|
|
|
@@ -292,9 +299,17 @@ function matchScore(text, query) {
|
|
|
292
299
|
}
|
|
293
300
|
|
|
294
301
|
// src/tui/world.ts
|
|
302
|
+
var failed = (message) => ({ message, failed: true });
|
|
303
|
+
var isFailure = (r) => typeof r !== "string" && "failed" in r;
|
|
304
|
+
var followUp = (r) => typeof r !== "string" && "then" in r ? r.then : void 0;
|
|
305
|
+
var said = (r) => typeof r === "string" ? r : r.message;
|
|
306
|
+
var joined = (results) => {
|
|
307
|
+
const message = results.map(said).join(" \xB7 ");
|
|
308
|
+
return results.some(isFailure) ? failed(message) : message;
|
|
309
|
+
};
|
|
295
310
|
var SEVERITY_RANK = { problem: 0, warning: 1, hint: 2 };
|
|
296
311
|
function filterOf(source) {
|
|
297
|
-
return source === "lib" || source === "repo" || source === "untracked" ? "local" : source === "global" ? "global" : source === "plugin" ? "plugins" : "vendor";
|
|
312
|
+
return source === "lib" || source === "repo" || source === "untracked" ? "local" : source === "global" || source === "skilllib" ? "global" : source === "plugin" ? "plugins" : "vendor";
|
|
298
313
|
}
|
|
299
314
|
function project(w, name) {
|
|
300
315
|
return w.projects.find((p) => p.name === name);
|
|
@@ -325,6 +340,10 @@ function lib(w, name) {
|
|
|
325
340
|
return w.library.find((l) => l.name === name);
|
|
326
341
|
}
|
|
327
342
|
var agentNames = (ids) => ids.map((a) => harness(a).name).join(", ");
|
|
343
|
+
function usedElsewhere(w, except, name) {
|
|
344
|
+
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);
|
|
345
|
+
return repos.length ? `, and ${repos.join(", ")} used it in the last 30 days` : "";
|
|
346
|
+
}
|
|
328
347
|
function dupeIssues(repo, name, d) {
|
|
329
348
|
const g = repo === null ? "-g" : "";
|
|
330
349
|
if (d.differ)
|
|
@@ -381,7 +400,7 @@ function globalFixes(m) {
|
|
|
381
400
|
label: "Move it to the repos that need it\u2026",
|
|
382
401
|
preview: `Pick repos. ${m.name} goes into your library and those repos, and stops loading globally (original backed up).`,
|
|
383
402
|
run: () => "",
|
|
384
|
-
pickRepos: (w, repos) => w.ops.moveGlobal(m, repos)
|
|
403
|
+
pickRepos: (w, repos) => w.ops.moveGlobal([m], repos)
|
|
385
404
|
},
|
|
386
405
|
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) },
|
|
387
406
|
{ label: "Delete it", preview: `Remove ${m.path}. A backup is kept in Settings \u203A Backups.`, run: (w) => w.ops.deleteGlobal(m) }
|
|
@@ -389,9 +408,10 @@ function globalFixes(m) {
|
|
|
389
408
|
}
|
|
390
409
|
function machineActions(m) {
|
|
391
410
|
if (m.broken) return [];
|
|
392
|
-
return m.source === "global" ? globalFixes(m) : [];
|
|
411
|
+
return m.source === "global" ? globalFixes(m) : m.source === "skilllib" ? [removeAgentSkillFix()] : [];
|
|
393
412
|
}
|
|
394
413
|
function machineIssues(w, m) {
|
|
414
|
+
if (m.source === "skilllib") return [agentSkillIssue(w)].flatMap((i) => i ? [i] : []);
|
|
395
415
|
if (m.broken)
|
|
396
416
|
return [
|
|
397
417
|
{
|
|
@@ -411,11 +431,12 @@ function machineIssues(w, m) {
|
|
|
411
431
|
severity: "problem",
|
|
412
432
|
title: `Loaded twice: also in ${other.source} ${other.where}`,
|
|
413
433
|
short: "Loaded twice",
|
|
414
|
-
|
|
434
|
+
// Every repo then runs the other copy instead of yours: a decision, never "fix all".
|
|
435
|
+
decision: true,
|
|
415
436
|
fixes: [
|
|
416
437
|
{
|
|
417
438
|
label: `Keep the ${other.source}'s copy, stop loading yours globally`,
|
|
418
|
-
preview: `Copy ${m.name} into your library if needed, then move ${m.path} to the backups.`,
|
|
439
|
+
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.`,
|
|
419
440
|
run: (w2) => w2.ops.unloadGlobal(m)
|
|
420
441
|
}
|
|
421
442
|
]
|
|
@@ -458,7 +479,16 @@ function issuesOf(w, projectName, u) {
|
|
|
458
479
|
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.`,
|
|
459
480
|
run: (w2) => w2.ops.remove(projectName, s.name)
|
|
460
481
|
};
|
|
461
|
-
if (s.missing)
|
|
482
|
+
if (s.missing && s.library === "missing")
|
|
483
|
+
issues.push({
|
|
484
|
+
id: `unavailable:${s.name}`,
|
|
485
|
+
severity: "problem",
|
|
486
|
+
title: "Pinned in skilllib.json, but not in your library: import it, or set SKILLLIB_HOME to the library that has it",
|
|
487
|
+
short: "Pinned, not in your library",
|
|
488
|
+
decision: true,
|
|
489
|
+
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) }]
|
|
490
|
+
});
|
|
491
|
+
else if (s.missing)
|
|
462
492
|
issues.push({
|
|
463
493
|
id: `missing:${s.name}`,
|
|
464
494
|
severity: "problem",
|
|
@@ -467,7 +497,7 @@ function issuesOf(w, projectName, u) {
|
|
|
467
497
|
decision: false,
|
|
468
498
|
fixes: [{ label: "Restore it (sync)", preview: `Reinstall ${s.name} v${s.version} into ${s.dir}.`, run: (w2) => w2.ops.restore(projectName, s.name) }]
|
|
469
499
|
});
|
|
470
|
-
if (s.source === "lib" && s.library === "missing")
|
|
500
|
+
else if (s.source === "lib" && s.library === "missing")
|
|
471
501
|
issues.push({
|
|
472
502
|
id: `not-in-lib:${s.name}`,
|
|
473
503
|
severity: "problem",
|
|
@@ -487,7 +517,7 @@ function issuesOf(w, projectName, u) {
|
|
|
487
517
|
...s.source !== "repo" ? [{ ...remove, label: "Remove this repo's copy, keep it global" }] : [],
|
|
488
518
|
{
|
|
489
519
|
label: "Stop loading it globally after all",
|
|
490
|
-
preview: `Copy ${s.name} into your library if needed, then move ${g.path} to the backups.
|
|
520
|
+
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)}.`,
|
|
491
521
|
run: (w2) => w2.ops.unloadGlobal(g)
|
|
492
522
|
}
|
|
493
523
|
]
|
|
@@ -498,17 +528,28 @@ function issuesOf(w, projectName, u) {
|
|
|
498
528
|
severity: "problem",
|
|
499
529
|
title: `Loaded twice: also global in ${g.where}`,
|
|
500
530
|
short: "Loaded twice",
|
|
501
|
-
|
|
531
|
+
// Unloading the global copy changes every repo: a decision, never "fix all".
|
|
532
|
+
decision: true,
|
|
502
533
|
fixes: [
|
|
503
534
|
{
|
|
504
535
|
label: "Keep this repo's copy, stop loading it globally",
|
|
505
|
-
preview: `Copy ${s.name} into your library if needed, then move ${g.path} to the backups.
|
|
536
|
+
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)}.`,
|
|
506
537
|
run: (w2) => w2.ops.unloadGlobal(g)
|
|
507
538
|
},
|
|
508
539
|
...s.source !== "repo" ? [{ ...remove, label: "Keep it global, remove it from this repo" }] : []
|
|
509
540
|
]
|
|
510
541
|
});
|
|
511
|
-
|
|
542
|
+
const cursorOnly = vendor?.source === "plugin" && w.agents.includes("claude-code") && !vendor.agents.includes("claude-code");
|
|
543
|
+
if (vendor && cursorOnly)
|
|
544
|
+
issues.push({
|
|
545
|
+
id: `twice-p:${s.name}`,
|
|
546
|
+
severity: "warning",
|
|
547
|
+
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`,
|
|
548
|
+
short: "Cursor also lists a plugin's copy",
|
|
549
|
+
decision: true,
|
|
550
|
+
fixes: [pluginFixFor(w, vendor.where)]
|
|
551
|
+
});
|
|
552
|
+
else if (vendor)
|
|
512
553
|
issues.push({
|
|
513
554
|
id: `twice-p:${s.name}`,
|
|
514
555
|
severity: "problem",
|
|
@@ -519,7 +560,7 @@ function issuesOf(w, projectName, u) {
|
|
|
519
560
|
...s.source !== "repo" ? [{ ...remove, label: `Remove this repo's copy, use the ${vendor.source}'s` }] : [],
|
|
520
561
|
vendor.source === "plugin" ? {
|
|
521
562
|
label: `Turn the plugin off in ${projectName} only`,
|
|
522
|
-
preview: `Sets "${vendor.where}": false in ${projectName}'s .claude/settings.local.json (claude plugin disable --scope local). Other repos keep it
|
|
563
|
+
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." : ""}`,
|
|
523
564
|
run: (w2) => w2.ops.pluginOffHere(projectName, vendor.where)
|
|
524
565
|
} : {
|
|
525
566
|
label: `Turn it off in ${vendor.where}`,
|
|
@@ -552,21 +593,34 @@ function issuesOf(w, projectName, u) {
|
|
|
552
593
|
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) }]
|
|
553
594
|
});
|
|
554
595
|
const blind = w.agents.filter((a) => !u.agents.includes(a));
|
|
555
|
-
|
|
596
|
+
const unshared = blind.filter((a) => s.unshared?.includes(a));
|
|
597
|
+
const linkable = blind.filter((a) => !unshared.includes(a));
|
|
598
|
+
const commitFirst = unshared.length ? ` ${agentNames(unshared)} ${unshared.length === 1 ? "needs" : "need"} it committed first (git tracks the folder they read).` : "";
|
|
599
|
+
if (!s.missing && linkable.length)
|
|
556
600
|
issues.push({
|
|
557
601
|
id: `blind:${s.name}`,
|
|
558
602
|
severity: "warning",
|
|
559
|
-
title: `${blind
|
|
560
|
-
short: `${blind
|
|
603
|
+
title: `${agentNames(blind)} can't see it`,
|
|
604
|
+
short: `${agentNames(blind)} can't see it`,
|
|
561
605
|
decision: false,
|
|
562
606
|
fixes: [
|
|
563
607
|
{
|
|
564
608
|
label: "Link it for every agent",
|
|
565
|
-
preview: `Add links (nothing is copied or moved), so ${
|
|
609
|
+
preview: `Add links (nothing is copied or moved), so ${agentNames(linkable)} ${linkable.length === 1 ? "loads" : "load"} it too.${commitFirst}`,
|
|
566
610
|
run: (w2) => w2.ops.link(projectName, s.name)
|
|
567
611
|
}
|
|
568
612
|
]
|
|
569
613
|
});
|
|
614
|
+
else if (!s.missing && unshared.length)
|
|
615
|
+
issues.push({
|
|
616
|
+
id: `blind:${s.name}`,
|
|
617
|
+
severity: "warning",
|
|
618
|
+
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)`,
|
|
619
|
+
short: `${agentNames(unshared)} can't see it: commit it first`,
|
|
620
|
+
// Nothing skilllib can do: committing is yours.
|
|
621
|
+
decision: true,
|
|
622
|
+
fixes: []
|
|
623
|
+
});
|
|
570
624
|
const differ = !!s.dupes?.differ;
|
|
571
625
|
if (s.source !== "lib" && s.library === "differs" && !differ)
|
|
572
626
|
issues.push({
|
|
@@ -586,7 +640,8 @@ function issuesOf(w, projectName, u) {
|
|
|
586
640
|
severity: "hint",
|
|
587
641
|
title: "Same as your library skill, but not tracked",
|
|
588
642
|
short: "Not tracked",
|
|
589
|
-
|
|
643
|
+
// It writes to the repo's skilllib.json (and your library): your call, never in Fix all (as in doctor, #11).
|
|
644
|
+
decision: true,
|
|
590
645
|
fixes: [{ label: "Track it", preview: `Record ${s.name} in skilllib.json so library updates reach it.`, run: (w2) => w2.ops.track(projectName, s.name) }]
|
|
591
646
|
});
|
|
592
647
|
if (s.source === "untracked" && !l && !vendor && !differ)
|
|
@@ -595,7 +650,8 @@ function issuesOf(w, projectName, u) {
|
|
|
595
650
|
severity: "hint",
|
|
596
651
|
title: "Only exists in this repo",
|
|
597
652
|
short: "Only in this repo",
|
|
598
|
-
|
|
653
|
+
// It writes to the repo's skilllib.json (and your library): your call, never in Fix all (as in doctor, #11).
|
|
654
|
+
decision: true,
|
|
599
655
|
fixes: [
|
|
600
656
|
{
|
|
601
657
|
label: "Import it into your library",
|
|
@@ -604,7 +660,7 @@ function issuesOf(w, projectName, u) {
|
|
|
604
660
|
}
|
|
605
661
|
]
|
|
606
662
|
});
|
|
607
|
-
if (s.source === "lib" && w.days && u.uses === 0 && !s.missing)
|
|
663
|
+
if (s.source === "lib" && w.days && u.uses === 0 && !s.missing && (s.added ?? 0) <= Date.now() - 30 * 24 * 60 * 60 * 1e3)
|
|
608
664
|
issues.push({
|
|
609
665
|
id: `unused:${s.name}`,
|
|
610
666
|
severity: "hint",
|
|
@@ -615,6 +671,53 @@ function issuesOf(w, projectName, u) {
|
|
|
615
671
|
});
|
|
616
672
|
return issues;
|
|
617
673
|
}
|
|
674
|
+
function agentSkillFix(w) {
|
|
675
|
+
return {
|
|
676
|
+
label: w.agentSkill === "outdated" ? "Update the skilllib skill" : "Install the skilllib skill for your agents",
|
|
677
|
+
preview: "One global skill, skilllib, where your agents look in every repo (~/.claude/skills, and ~/.agents/skills for Codex and Zed). Then ask your agents which skills they can use here, where each comes from, and which of your skills a repo should add. `skilllib agent-skill remove` takes it out.",
|
|
678
|
+
run: (w2) => w2.ops.installAgentSkill()
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
function removeAgentSkillFix() {
|
|
682
|
+
return {
|
|
683
|
+
label: "Remove the skilllib skill",
|
|
684
|
+
preview: "Removes it from ~/.claude/skills and ~/.agents/skills, where skilllib put it. Your agents then stop using skilllib to answer which skills they can use. Settings \u203A skilllib skill puts it back.",
|
|
685
|
+
run: (w) => w.ops.removeAgentSkill()
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
function agentSkillIssue(w) {
|
|
689
|
+
if (w.agentSkill === "installed" || !w.agents.length) return null;
|
|
690
|
+
const missing = w.agentSkill === "missing";
|
|
691
|
+
return {
|
|
692
|
+
id: "agent-skill",
|
|
693
|
+
severity: missing ? "hint" : "warning",
|
|
694
|
+
title: missing ? "Your agents don't know about skilllib" : "The skilllib skill for your agents is out of date",
|
|
695
|
+
short: missing ? "Your agents don't know about skilllib" : "Out of date",
|
|
696
|
+
decision: missing,
|
|
697
|
+
fixes: [agentSkillFix(w)]
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
function runAll(w, fixes) {
|
|
701
|
+
const held = [];
|
|
702
|
+
const bad = [];
|
|
703
|
+
for (const fix of fixes) {
|
|
704
|
+
let r;
|
|
705
|
+
try {
|
|
706
|
+
r = fix.run(w);
|
|
707
|
+
} catch (e) {
|
|
708
|
+
r = failed(e.message);
|
|
709
|
+
}
|
|
710
|
+
if (isFailure(r)) bad.push(said(r));
|
|
711
|
+
else if (followUp(r)) held.push(said(r));
|
|
712
|
+
}
|
|
713
|
+
const ok = fixes.length - held.length - bad.length;
|
|
714
|
+
const message = [
|
|
715
|
+
`Applied ${ok}${ok === fixes.length ? "" : ` of ${fixes.length}`} fix${fixes.length === 1 ? "" : "es"}${ok ? " (anything removed is in Settings \u203A Backups)" : ""}`,
|
|
716
|
+
held.length ? `${held.join("; ")} (fix ${held.length === 1 ? "it" : "them"} on ${held.length === 1 ? "its" : "their"} own to go ahead)` : "",
|
|
717
|
+
bad.length ? `couldn't: ${bad.join("; ")}` : ""
|
|
718
|
+
].filter(Boolean).join(" \xB7 ");
|
|
719
|
+
return bad.length ? failed(message) : message;
|
|
720
|
+
}
|
|
618
721
|
function worst(issues) {
|
|
619
722
|
return [...issues].sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity])[0];
|
|
620
723
|
}
|
|
@@ -643,28 +746,120 @@ function deleteLibraryFix(w, names) {
|
|
|
643
746
|
"Every copy goes to Settings \u203A Backups.",
|
|
644
747
|
others.length ? `Untracked or the team's own copies in ${others.join(", ")} stay.` : ""
|
|
645
748
|
].filter(Boolean).join(" "),
|
|
646
|
-
run: (w2) => names.map((n) => w2.ops.deleteLibrary(n))
|
|
749
|
+
run: (w2) => joined(names.map((n) => w2.ops.deleteLibrary(n)))
|
|
647
750
|
};
|
|
648
751
|
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
752
|
+
var plural = (n, what) => `${n} ${what}${n === 1 ? "" : "s"}`;
|
|
753
|
+
function pluginName(p) {
|
|
754
|
+
return p.id.split("@")[0];
|
|
755
|
+
}
|
|
756
|
+
function pluginSkillKey(p, skill) {
|
|
757
|
+
return `${pluginName(p)}:${skill}`;
|
|
758
|
+
}
|
|
759
|
+
function pluginUses(w, p, repo) {
|
|
760
|
+
return p.skills.reduce((n, s) => n + (repo ? w.usage[repo]?.[pluginSkillKey(p, s.name)] ?? 0 : totalUses(w, pluginSkillKey(p, s.name))), 0);
|
|
761
|
+
}
|
|
762
|
+
function pluginScope(p) {
|
|
763
|
+
return p.repo ? `${p.scope} ${p.repo}` : p.scope;
|
|
764
|
+
}
|
|
765
|
+
function replacePluginFix(p) {
|
|
766
|
+
const names = p.skills.map((s) => s.name);
|
|
767
|
+
const synced = p.scope === "claude.ai";
|
|
768
|
+
const offOnly = synced || !!p.settingsOnly;
|
|
654
769
|
return {
|
|
655
|
-
label: `Replace ${
|
|
770
|
+
label: `Replace ${pluginName(p)} with library skills\u2026`,
|
|
656
771
|
preview: [
|
|
657
|
-
`Copies its ${
|
|
658
|
-
synced ? `then turns ${id} off in Claude Code. It's synced from your claude.ai account: to delete it for good, remove it there.` : `then uninstalls ${id} from
|
|
659
|
-
parts.length ? `It also brings ${parts.join(", ")}: those ${
|
|
660
|
-
|
|
772
|
+
`Copies its ${plural(names.length, "skill")} into your library and adds them to the repos you tick (or none),`,
|
|
773
|
+
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).`,
|
|
774
|
+
p.parts.length ? `It also brings ${p.parts.join(", ")}: those ${offOnly ? "stop" : "go"} too.` : "",
|
|
775
|
+
offOnly ? "Turn it back on from Plugins." : "Settings \u203A Backups reinstalls it."
|
|
661
776
|
].filter(Boolean).join(" "),
|
|
662
777
|
run: () => "",
|
|
663
778
|
allowNone: true,
|
|
664
|
-
preticked: (
|
|
665
|
-
pickRepos: (
|
|
779
|
+
preticked: (w) => w.projects.filter((x) => pluginUses(w, p, x.name) > 0).map((x) => x.name),
|
|
780
|
+
pickRepos: (w, repos) => w.ops.replacePlugin(p, repos)
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
function pluginFixFor(w, id) {
|
|
784
|
+
const ofId = w.plugins.filter((p2) => p2.agent === "claude-code" && p2.id === id);
|
|
785
|
+
const p = ofId.find((x) => !x.repo) ?? ofId[0];
|
|
786
|
+
if (p) return replacePluginFix(p);
|
|
787
|
+
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` };
|
|
788
|
+
}
|
|
789
|
+
function pluginSwitchFix(p, on) {
|
|
790
|
+
const where = p.repo ? ` in ${p.repo} (${p.scope} install)` : p.scope === "claude.ai" ? " on this machine" : " in every repo";
|
|
791
|
+
return {
|
|
792
|
+
label: on ? "Turn it on" : "Turn it off",
|
|
793
|
+
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.`,
|
|
794
|
+
run: (w) => w.ops.setPlugin(p, on)
|
|
666
795
|
};
|
|
667
796
|
}
|
|
797
|
+
function uninstallPluginFix(p) {
|
|
798
|
+
return {
|
|
799
|
+
label: "Uninstall",
|
|
800
|
+
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.`,
|
|
801
|
+
run: (w) => w.ops.uninstallPlugin(p)
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
function updatePluginFix(p) {
|
|
805
|
+
return {
|
|
806
|
+
label: p.update === "newer" ? "Update it" : `Update to ${p.update}`,
|
|
807
|
+
preview: `Runs claude plugin update ${p.id}${p.repo ? ` in ${p.repo}` : ""}. Updates can bring new commands, hooks or MCP servers. If the marketplace asks to run a command, skilllib stops and you update it yourself. Claude Code uses the new version after a restart.`,
|
|
808
|
+
run: (w) => w.ops.updatePlugin(p)
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
function pluginIssues(w, p) {
|
|
812
|
+
const library = new Set(w.library.map((l) => l.name));
|
|
813
|
+
const global = new Set(w.machine.filter((m) => m.source === "global" && !m.broken).map((m) => m.name));
|
|
814
|
+
if (p.agent === "cursor") {
|
|
815
|
+
const dupes = p.skills.filter((s) => library.has(s.name) || global.has(s.name));
|
|
816
|
+
return dupes.length ? [
|
|
817
|
+
{
|
|
818
|
+
id: `plugin-dupes:${p.key}`,
|
|
819
|
+
severity: "warning",
|
|
820
|
+
title: `Repeats your ${dupes.map((s) => s.name).join(", ")}: Cursor lists both while it's on`,
|
|
821
|
+
short: `Repeats ${dupes.length} of yours`,
|
|
822
|
+
decision: true,
|
|
823
|
+
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` }]
|
|
824
|
+
}
|
|
825
|
+
] : [];
|
|
826
|
+
}
|
|
827
|
+
const issues = [];
|
|
828
|
+
if (p.on) {
|
|
829
|
+
const repo = p.repo ? w.projects.find((x) => x.name === p.repo) : void 0;
|
|
830
|
+
const here = repo ? new Set(repo.skills.map((s) => s.name)) : void 0;
|
|
831
|
+
const yours = (n) => global.has(n) || (p.repo ? !!here?.has(n) : library.has(n));
|
|
832
|
+
const dupes = p.skills.filter((s) => yours(s.name));
|
|
833
|
+
if (dupes.length)
|
|
834
|
+
issues.push({
|
|
835
|
+
id: `plugin-dupes:${p.key}`,
|
|
836
|
+
severity: "warning",
|
|
837
|
+
title: `Repeats your ${dupes.map((s) => s.name).join(", ")}`,
|
|
838
|
+
short: `Repeats ${dupes.length} of yours`,
|
|
839
|
+
decision: true,
|
|
840
|
+
fixes: [replacePluginFix(p), pluginSwitchFix(p, false)]
|
|
841
|
+
});
|
|
842
|
+
if (w.days && p.skills.length && pluginUses(w, p) === 0)
|
|
843
|
+
issues.push({
|
|
844
|
+
id: `plugin-unused:${p.key}`,
|
|
845
|
+
severity: "hint",
|
|
846
|
+
title: `Its skills weren't used in 30 days (Claude Code)${p.parts.length ? `; it also brings ${p.parts.join(", ")}` : ""}`,
|
|
847
|
+
short: "Unused 30 days",
|
|
848
|
+
decision: true,
|
|
849
|
+
fixes: [pluginSwitchFix(p, false), ...p.scope === "claude.ai" || p.settingsOnly ? [] : [uninstallPluginFix(p)]]
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
if (p.update)
|
|
853
|
+
issues.push({
|
|
854
|
+
id: `plugin-update:${p.key}`,
|
|
855
|
+
severity: "hint",
|
|
856
|
+
title: p.update === "newer" ? "Its marketplace has a newer commit" : `Update available: ${p.version ?? "?"} \u2192 ${p.update}`,
|
|
857
|
+
short: p.update === "newer" ? "Update available" : `Update to ${p.update}`,
|
|
858
|
+
decision: true,
|
|
859
|
+
fixes: [updatePluginFix(p)]
|
|
860
|
+
});
|
|
861
|
+
return issues;
|
|
862
|
+
}
|
|
668
863
|
function originGroup(origin) {
|
|
669
864
|
if (!origin) return null;
|
|
670
865
|
const plugin = origin.match(/^plugin: (.+)$/);
|
|
@@ -689,7 +884,9 @@ function groupCandidates(w, u) {
|
|
|
689
884
|
if (m?.source === "plugin") return [{ key: `plugin:${m.where}`, label: `\u29C9 ${m.where.split("@")[0]} plugin` }];
|
|
690
885
|
if (m?.source === "claude.ai") return [{ key: "claude.ai", label: "claude.ai skills" }];
|
|
691
886
|
if (m?.source === "cursor") return [{ key: "cursor", label: "Cursor built-ins" }];
|
|
887
|
+
if (m?.source === "grok") return [{ key: "grok", label: "Grok bundled" }];
|
|
692
888
|
if (m?.source === "system") return [{ key: `system:${m.where}`, label: m.where }];
|
|
889
|
+
if (m?.source === "skilllib") return [];
|
|
693
890
|
if (m) {
|
|
694
891
|
const origin = originGroup(m.origin);
|
|
695
892
|
return [...origin ? [origin] : [], ...byTime("global", m.installed), ...byWord("global", m.name)];
|
|
@@ -733,10 +930,8 @@ function addRows(w, target, q) {
|
|
|
733
930
|
name: q ? `+ Create "${q}"${where}` : "+ Create a new skill (type its name)",
|
|
734
931
|
note: "",
|
|
735
932
|
description: "",
|
|
736
|
-
run: (w2) =>
|
|
737
|
-
|
|
738
|
-
return target ? w2.ops.add(target, q) : made;
|
|
739
|
-
}
|
|
933
|
+
run: (w2) => w2.ops.createSkill(q),
|
|
934
|
+
...target ? { afterEdit: (w2) => w2.ops.add(target, q) } : {}
|
|
740
935
|
},
|
|
741
936
|
{ key: "agent", agent: true, name: q ? `\u2726 Write "${q}" with an agent` : "\u2726 Write a new skill with an agent", note: "", description: "" }
|
|
742
937
|
];
|
|
@@ -745,9 +940,32 @@ function addRows(w, target, q) {
|
|
|
745
940
|
}
|
|
746
941
|
|
|
747
942
|
// src/tui/prompts.ts
|
|
748
|
-
|
|
943
|
+
var agentNames2 = (agents) => agents.map((a) => harness(a).name).join(", ") || "none of my agents";
|
|
944
|
+
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)";
|
|
945
|
+
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)");
|
|
946
|
+
function reviewPrompt(w, names, scope, repo) {
|
|
749
947
|
const skills = names.map((name) => {
|
|
750
948
|
const copies = w.machine.filter((m2) => m2.name === name);
|
|
949
|
+
const here = repo ? w.projects.find((p) => p.name === repo)?.skills.find((s) => s.name === name) : void 0;
|
|
950
|
+
if (here) {
|
|
951
|
+
const lib3 = w.library.find((l) => l.name === name);
|
|
952
|
+
return {
|
|
953
|
+
name,
|
|
954
|
+
description: w.descriptions[name] ?? "",
|
|
955
|
+
source: localSource(here),
|
|
956
|
+
installedHow: localInstalled(here),
|
|
957
|
+
path: here.path,
|
|
958
|
+
links: [],
|
|
959
|
+
loadedBy: `${agentNames2(here.agents)} \u2014 in ${repo}`,
|
|
960
|
+
vendor: null,
|
|
961
|
+
usesTotal: w.days ? totalUses(w, name) : null,
|
|
962
|
+
usesByProject: Object.entries(w.usage).flatMap(([p, u]) => u[name] ? [[p, u[name]]] : []),
|
|
963
|
+
installedIn: w.projects.filter((p) => p.skills.some((s) => s.name === name)).map((p) => p.name),
|
|
964
|
+
otherCopies: copies.map((c) => `${c.source} ${c.where}`),
|
|
965
|
+
inYourSkills: lib3 ? `yes, v${lib3.latest}` : null,
|
|
966
|
+
keptGlobal: void 0
|
|
967
|
+
};
|
|
968
|
+
}
|
|
751
969
|
const m = copies.find((c) => c.source === "global") ?? copies[0];
|
|
752
970
|
const lib2 = w.library.find((l) => l.name === name);
|
|
753
971
|
const repos = w.projects.filter((p) => p.skills.some((s) => s.name === name));
|
|
@@ -755,11 +973,11 @@ function reviewPrompt(w, names, scope) {
|
|
|
755
973
|
return {
|
|
756
974
|
name,
|
|
757
975
|
description: w.descriptions[name] ?? "",
|
|
758
|
-
source: m ? m.source === "global" ? "my global skills" : `vendor: ${m.source} (${m.where})` : lib2 ? "Your skills (skilllib library)" : "a repo's own skill",
|
|
759
|
-
installedHow: m?.origin ?? (m ? m.where : lib2 ? "created or imported into skilllib" : "committed in the repo"),
|
|
976
|
+
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",
|
|
977
|
+
installedHow: m?.origin ?? (m ? m.where : lib2 ? "created or imported into skilllib" : local ? localInstalled(local) : "committed in the repo"),
|
|
760
978
|
path: m?.path ?? (lib2 ? w.ops.libraryFile(name).replace(/\/SKILL\.md$/, "") : local?.path ?? ""),
|
|
761
979
|
links: m?.links ?? [],
|
|
762
|
-
loadedBy: m ? `${m.agents.map((
|
|
980
|
+
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",
|
|
763
981
|
vendor: m && m.source !== "global" ? "turn it off at its source (/plugin in Claude Code, claude.ai settings, or Cursor)" : null,
|
|
764
982
|
usesTotal: w.days ? totalUses(w, name) : null,
|
|
765
983
|
usesByProject: Object.entries(w.usage).flatMap(([p, u]) => u[name] ? [[p, u[name]]] : []),
|
|
@@ -792,13 +1010,12 @@ Keep it under 200 lines. ${p ? "skilllib will offer to import it into my library
|
|
|
792
1010
|
|
|
793
1011
|
// src/tui/App.tsx
|
|
794
1012
|
import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
795
|
-
|
|
796
|
-
return p.startsWith(userHome()) ? "~" + p.slice(userHome().length) : p;
|
|
797
|
-
}
|
|
1013
|
+
var tildify3 = (p) => tildify(p);
|
|
798
1014
|
var PLACES = [
|
|
799
1015
|
["projects", "Projects"],
|
|
800
1016
|
["library", "Your skills"],
|
|
801
1017
|
["global", "Global"],
|
|
1018
|
+
["plugins", "Plugins"],
|
|
802
1019
|
["health", "Health"],
|
|
803
1020
|
["settings", "Settings"]
|
|
804
1021
|
];
|
|
@@ -826,6 +1043,7 @@ function tag(u, w) {
|
|
|
826
1043
|
if (s) return [s.source, color.blue];
|
|
827
1044
|
if (u.source === "global") return [u.machine?.kept ? "global \u2713" : "global", color.yellow];
|
|
828
1045
|
if (u.source === "plugin") return [`\u29C9 ${u.where.split("@")[0]}`, color.magenta];
|
|
1046
|
+
if (u.source === "skilllib") return ["\u25C6 skilllib", color.accent];
|
|
829
1047
|
return [u.source, color.muted];
|
|
830
1048
|
}
|
|
831
1049
|
function isNameChar(input, key) {
|
|
@@ -876,7 +1094,7 @@ function TitleRow({ cells, width }) {
|
|
|
876
1094
|
}
|
|
877
1095
|
var fixOption = (fix) => ({ label: fix.label, fix });
|
|
878
1096
|
var GIT_LABEL = { committed: "\u2713 committed", changed: "\xB1 uncommitted changes", new: "+ not committed", ignored: "\u2205 gitignored" };
|
|
879
|
-
var COPY_RANK = { lib: 0, repo: 0, untracked: 0, global: 1, plugin: 2, "claude.ai": 3, cursor: 3, system: 3 };
|
|
1097
|
+
var COPY_RANK = { lib: 0, repo: 0, untracked: 0, global: 1, skilllib: 1, plugin: 2, "claude.ai": 3, cursor: 3, grok: 3, system: 3 };
|
|
880
1098
|
function byName(copies) {
|
|
881
1099
|
const groups = /* @__PURE__ */ new Map();
|
|
882
1100
|
for (const c of copies) groups.set(c.name, [...groups.get(c.name) ?? [], c]);
|
|
@@ -944,7 +1162,7 @@ function skillItem(w, copies, issues, nameW, actions, repo) {
|
|
|
944
1162
|
const b = builder(u.name);
|
|
945
1163
|
for (const l of wrap(w.descriptions[u.name] || "(no description)", width - 4, 3)) b.line([l, color.muted]);
|
|
946
1164
|
b.line();
|
|
947
|
-
if (repo && !u.local) b.line(["Loads in every repo", color.text], [u.source === "global" ? " issues and cleanup live in Global" : " managed at its source", color.faint]);
|
|
1165
|
+
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]);
|
|
948
1166
|
else b.issues(issues);
|
|
949
1167
|
b.actions(actions, width);
|
|
950
1168
|
b.line();
|
|
@@ -953,7 +1171,7 @@ function skillItem(w, copies, issues, nameW, actions, repo) {
|
|
|
953
1171
|
b.line(["Loaded from", color.accent]);
|
|
954
1172
|
for (const c of copies) {
|
|
955
1173
|
const s = c.local;
|
|
956
|
-
const path = s ? `${s.dir}/${c.name}` : c.source === "global" ? `${c.where}/${c.name}` : c.where;
|
|
1174
|
+
const path = s ? `${s.dir}/${c.name}` : c.source === "global" || c.source === "skilllib" ? `${c.where}/${c.name}` : c.where;
|
|
957
1175
|
const extra = s ? s.missing ? "missing" : s.git ? GIT_LABEL[s.git] : "" : "";
|
|
958
1176
|
const [ct, cc] = c === u ? ["", void 0] : tag(c, w);
|
|
959
1177
|
b.line([" ", void 0], [path + " ", color.text], [ct ? ct + " " : "", cc], [extra, color.faint]);
|
|
@@ -967,7 +1185,10 @@ function projectSkillItems(w, p, nameW, ui) {
|
|
|
967
1185
|
(copies) => skillItem(w, copies, mergeIssues(copies.filter((c) => c.local).map((c) => issuesOf(w, p, c))), nameW, skillActions(w, p, copies[0], ui), p)
|
|
968
1186
|
);
|
|
969
1187
|
}
|
|
970
|
-
var reviewOption = (w, name, ui
|
|
1188
|
+
var reviewOption = (w, name, ui, repo) => ({
|
|
1189
|
+
label: "Review prompt",
|
|
1190
|
+
action: () => ui.copy(reviewPrompt(w, [name], `Review the skill ${name}${repo ? ` in ${repo}` : ""}.`, repo), `a review prompt for ${name}`)
|
|
1191
|
+
});
|
|
971
1192
|
function versionsOption(w, repo, name, installed, ui) {
|
|
972
1193
|
return {
|
|
973
1194
|
label: "Other versions\u2026",
|
|
@@ -980,27 +1201,35 @@ function versionsOption(w, repo, name, installed, ui) {
|
|
|
980
1201
|
function skillActions(w, p, u, ui) {
|
|
981
1202
|
const s = u.local;
|
|
982
1203
|
const inLibrary = w.library.find((l) => l.name === u.name);
|
|
983
|
-
const remove = {
|
|
1204
|
+
const remove = {
|
|
1205
|
+
// Its folder gone: all that's left to remove is the skilllib.json entry (the label Health offers too).
|
|
1206
|
+
label: s?.missing ? "Remove it from skilllib.json" : "Remove from repo",
|
|
1207
|
+
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.`,
|
|
1208
|
+
run: (w2) => w2.ops.remove(p, u.name)
|
|
1209
|
+
};
|
|
984
1210
|
if (s?.source === "lib")
|
|
985
1211
|
return [
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1212
|
+
// A tracked skill your library doesn't have: nothing to edit there, no versions to pick.
|
|
1213
|
+
...inLibrary ? [
|
|
1214
|
+
...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) })] : [],
|
|
1215
|
+
{ label: "Edit in library", action: () => ui.edit(w.ops.libraryFile(u.name)) },
|
|
1216
|
+
versionsOption(w, p, u.name, s.version, ui)
|
|
1217
|
+
] : [],
|
|
989
1218
|
fixOption(remove),
|
|
990
|
-
reviewOption(w, u.name, ui)
|
|
1219
|
+
reviewOption(w, u.name, ui, p)
|
|
991
1220
|
];
|
|
992
1221
|
if (s?.source === "repo")
|
|
993
1222
|
return [
|
|
994
1223
|
...inLibrary ? [] : [fixOption({ label: "Copy into library", preview: `Copy ${u.name} into your library, so other repos can add it. The repo's copy stays as it is.`, run: (w2) => w2.ops.copyToLibrary(p, u.name) })],
|
|
995
1224
|
{ label: "Edit SKILL.md", action: () => ui.edit(`${s.path}/SKILL.md`) },
|
|
996
|
-
reviewOption(w, u.name, ui)
|
|
1225
|
+
reviewOption(w, u.name, ui, p)
|
|
997
1226
|
];
|
|
998
1227
|
if (s)
|
|
999
1228
|
return [
|
|
1000
1229
|
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) }),
|
|
1001
1230
|
{ label: "Edit SKILL.md", action: () => ui.edit(`${s.path}/SKILL.md`) },
|
|
1002
1231
|
fixOption(remove),
|
|
1003
|
-
reviewOption(w, u.name, ui)
|
|
1232
|
+
reviewOption(w, u.name, ui, p)
|
|
1004
1233
|
];
|
|
1005
1234
|
return [{ label: "Open in Global", action: () => ui.openInGlobal(u.name) }, reviewOption(w, u.name, ui)];
|
|
1006
1235
|
}
|
|
@@ -1008,11 +1237,13 @@ function overview(w, p) {
|
|
|
1008
1237
|
const project2 = project(w, p);
|
|
1009
1238
|
const info = w.ops.repoInfo(p);
|
|
1010
1239
|
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];
|
|
1240
|
+
const plugins = [...new Set(machineIn(w, project2).filter((m) => m.source === "plugin").map((m) => m.where.split("@")[0]))];
|
|
1011
1241
|
return [
|
|
1012
|
-
["Folder", [[
|
|
1242
|
+
["Folder", [[tildify3(project2.path), color.text]]],
|
|
1013
1243
|
["Remote", [[info.remote ?? "none", info.remote ? color.blue : color.faint]]],
|
|
1014
1244
|
["Branch", [[info.branch ?? "\u2013", color.text], [info.dirty ? ` ${info.dirty} uncommitted` : "", color.yellow]]],
|
|
1015
|
-
["Manifest", [manifest]]
|
|
1245
|
+
["Manifest", [manifest]],
|
|
1246
|
+
["Plugins", [[plugins.length ? plugins.join(", ") : "none", plugins.length ? color.magenta : color.faint]]]
|
|
1016
1247
|
];
|
|
1017
1248
|
}
|
|
1018
1249
|
function InfoGrid({ rows, columns, width }) {
|
|
@@ -1054,13 +1285,21 @@ function globalItems(w, nameW, ui) {
|
|
|
1054
1285
|
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 }));
|
|
1055
1286
|
return byName(copies).map((g) => skillItem(w, g, mergeIssues(g.map((c) => machineIssues(w, c.machine))), nameW, globalActions(w, g[0], ui), null));
|
|
1056
1287
|
}
|
|
1288
|
+
var AT_SOURCE = {
|
|
1289
|
+
global: null,
|
|
1290
|
+
plugin: null,
|
|
1291
|
+
skilllib: null,
|
|
1292
|
+
"claude.ai": "Turn it off in claude.ai \u203A Settings",
|
|
1293
|
+
cursor: "Cursor manages it",
|
|
1294
|
+
grok: "Ships with Grok; a same-named skill of yours overrides it"
|
|
1295
|
+
};
|
|
1057
1296
|
function globalActions(w, u, ui) {
|
|
1058
1297
|
const m = u.machine;
|
|
1059
|
-
const atSource = m.source === "
|
|
1298
|
+
const atSource = m.source === "system" ? `An admin manages ${m.where}` : AT_SOURCE[m.source];
|
|
1060
1299
|
return [
|
|
1061
1300
|
...machineActions(m).map(fixOption),
|
|
1062
|
-
...m.source === "plugin" ? [
|
|
1063
|
-
...
|
|
1301
|
+
...m.source === "plugin" ? [{ label: `Open ${m.where.split("@")[0]} in Plugins`, action: () => ui.openInPlugins(m.where) }] : [],
|
|
1302
|
+
...atSource ? [{ label: atSource, action: () => ui.notify(`${atSource}; skilllib picks up the change next time`) }] : [],
|
|
1064
1303
|
...m.source === "global" && !m.broken ? [{ label: "Edit SKILL.md", action: () => ui.edit(`${m.path}/SKILL.md`) }] : [],
|
|
1065
1304
|
reviewOption(w, m.name, ui)
|
|
1066
1305
|
];
|
|
@@ -1068,8 +1307,9 @@ function globalActions(w, u, ui) {
|
|
|
1068
1307
|
function libraryItems(w, nameW, ui) {
|
|
1069
1308
|
return w.library.map((l) => {
|
|
1070
1309
|
const repos = w.projects.filter((p) => p.skills.some((s) => s.name === l.name));
|
|
1310
|
+
const removable = repos.filter((p) => p.skills.some((s) => s.name === l.name && s.source !== "repo"));
|
|
1071
1311
|
const uses = totalUses(w, l.name);
|
|
1072
|
-
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) =>
|
|
1312
|
+
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]) };
|
|
1073
1313
|
const delFix = deleteLibraryFix(w, [l.name]);
|
|
1074
1314
|
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] }];
|
|
1075
1315
|
const behind = repos.filter((p) => p.skills.some((s) => s.name === l.name && s.source === "lib" && s.version < l.latest)).length;
|
|
@@ -1101,13 +1341,13 @@ function libraryItems(w, nameW, ui) {
|
|
|
1101
1341
|
b.issues(issues);
|
|
1102
1342
|
b.actions([
|
|
1103
1343
|
fixOption(addFix),
|
|
1104
|
-
...
|
|
1344
|
+
...removable.length ? [
|
|
1105
1345
|
fixOption({
|
|
1106
1346
|
label: "Remove from repos\u2026",
|
|
1107
1347
|
preview: `Pick repos to remove ${l.name} from. Your library keeps it.`,
|
|
1108
1348
|
run: () => "",
|
|
1109
|
-
candidates: () =>
|
|
1110
|
-
pickRepos: (w2, rs) => rs.map((r) =>
|
|
1349
|
+
candidates: () => removable.map((p) => p.name),
|
|
1350
|
+
pickRepos: (w2, rs) => joined(rs.map((r) => w2.ops.remove(r, l.name)))
|
|
1111
1351
|
})
|
|
1112
1352
|
] : [],
|
|
1113
1353
|
fixOption(delFix),
|
|
@@ -1123,15 +1363,99 @@ function libraryItems(w, nameW, ui) {
|
|
|
1123
1363
|
}
|
|
1124
1364
|
function healthItems(w, nameW, ui) {
|
|
1125
1365
|
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]] }));
|
|
1126
|
-
return [...w.projects.flatMap((p) => flagged(p.name, projectSkillItems(w, p.name, nameW, ui))), ...flagged("Global", globalItems(w, nameW, ui))];
|
|
1366
|
+
return [...w.projects.flatMap((p) => flagged(p.name, projectSkillItems(w, p.name, nameW, ui))), ...flagged("Global", globalItems(w, nameW, ui)), ...agentSkillItems(w, nameW)];
|
|
1367
|
+
}
|
|
1368
|
+
function agentSkillItems(w, nameW) {
|
|
1369
|
+
const issue = agentSkillIssue(w);
|
|
1370
|
+
if (!issue || w.machine.some((m) => m.source === "skilllib")) return [];
|
|
1371
|
+
const sev = SEV[issue.severity];
|
|
1372
|
+
return [
|
|
1373
|
+
{
|
|
1374
|
+
key: "agent-skill",
|
|
1375
|
+
name: "skilllib",
|
|
1376
|
+
issues: [issue],
|
|
1377
|
+
uses: 0,
|
|
1378
|
+
cells: [
|
|
1379
|
+
{ text: sev.icon, width: 2, color: sev.color },
|
|
1380
|
+
{ text: clip("skilllib", nameW), width: nameW },
|
|
1381
|
+
{ text: clip("Your agents", 16), width: 16, color: color.muted },
|
|
1382
|
+
{ text: " " + issue.short, grow: true, color: sev.color }
|
|
1383
|
+
],
|
|
1384
|
+
detail: (width) => {
|
|
1385
|
+
const b = builder("skilllib");
|
|
1386
|
+
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]);
|
|
1387
|
+
b.line();
|
|
1388
|
+
b.issues([issue]);
|
|
1389
|
+
return b.d;
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
];
|
|
1393
|
+
}
|
|
1394
|
+
var pluginW = (nameW) => Math.min(nameW, 17);
|
|
1395
|
+
var STATE = { on: ["on", color.green], off: ["off", color.faint], unknown: ["?", color.faint] };
|
|
1396
|
+
function pluginItems(w, skillW, ui, issuesOf2) {
|
|
1397
|
+
const nameW = pluginW(skillW);
|
|
1398
|
+
return w.plugins.map((p) => {
|
|
1399
|
+
const issues = issuesOf2.get(p.key) ?? [];
|
|
1400
|
+
const top = worst(issues);
|
|
1401
|
+
const cursor = p.agent === "cursor";
|
|
1402
|
+
const uses = cursor ? 0 : pluginUses(w, p);
|
|
1403
|
+
const [state, stateColor] = STATE[p.on === null ? "unknown" : p.on ? "on" : "off"];
|
|
1404
|
+
const n = p.skills.length;
|
|
1405
|
+
const name = pluginName(p);
|
|
1406
|
+
const actions = cursor ? [{ label: "Turn it off in Cursor (Settings \u203A Plugins)", action: () => ui.notify(`Turn ${p.id} off in Cursor; skilllib can't read or change Cursor's plugins`) }] : [
|
|
1407
|
+
fixOption(pluginSwitchFix(p, !p.on)),
|
|
1408
|
+
...p.update ? [fixOption(updatePluginFix(p))] : [],
|
|
1409
|
+
...n ? [fixOption(replacePluginFix(p))] : [],
|
|
1410
|
+
...p.scope === "claude.ai" || p.settingsOnly ? [] : [fixOption(uninstallPluginFix(p))]
|
|
1411
|
+
];
|
|
1412
|
+
return {
|
|
1413
|
+
key: p.key,
|
|
1414
|
+
name: p.id,
|
|
1415
|
+
plugin: p,
|
|
1416
|
+
issues,
|
|
1417
|
+
uses,
|
|
1418
|
+
cells: [
|
|
1419
|
+
{ text: top ? SEV[top.severity].icon : " ", width: 2, color: top ? SEV[top.severity].color : void 0 },
|
|
1420
|
+
{ text: clip(name, nameW), width: nameW, color: p.on === false ? color.muted : void 0 },
|
|
1421
|
+
{ text: harness(p.agent).icon, width: 3, color: harness(p.agent).color },
|
|
1422
|
+
{ text: clip(cursor ? "\u2013" : pluginScope(p), 19), width: 19, color: p.repo ? color.blue : color.muted },
|
|
1423
|
+
{ text: clip(`${n} skill${n === 1 ? "" : "s"}${p.parts.length ? ` +${p.parts.length}` : ""}`, 13), width: 13, color: color.muted },
|
|
1424
|
+
{ text: state.padEnd(4), width: 4, color: stateColor },
|
|
1425
|
+
{ text: cursor ? " \u2013 " : usesText(uses), width: 6, color: color.muted },
|
|
1426
|
+
{ text: " " + (top?.short ?? ""), grow: true, color: top ? SEV[top.severity].color : color.faint }
|
|
1427
|
+
],
|
|
1428
|
+
detail: (width) => {
|
|
1429
|
+
const b = builder(p.id);
|
|
1430
|
+
for (const l of wrap(p.description || "(no description)", width - 4, 2)) b.line([l, color.muted]);
|
|
1431
|
+
b.line();
|
|
1432
|
+
b.issues(issues);
|
|
1433
|
+
b.actions([...actions, { label: "Open its folder", action: () => ui.notify(said2(w.ops.openFolder(p.path))) }, {
|
|
1434
|
+
label: "Review prompt",
|
|
1435
|
+
action: () => ui.copy(
|
|
1436
|
+
reviewPrompt(w, p.skills.map((s) => s.name), `Review the ${harness(p.agent).name} plugin ${p.id}${p.description ? ` (${p.description})` : ""}: these are its skills${p.parts.length ? `, and it also brings ${p.parts.join(", ")}` : ""}. Should I keep it ${p.on === false ? "off" : "on"}, turn it ${p.on === false ? "on" : "off"}, uninstall it, or replace it with my own copies of the skills I use?`),
|
|
1437
|
+
`a review prompt for ${name}`
|
|
1438
|
+
)
|
|
1439
|
+
}], width);
|
|
1440
|
+
b.line();
|
|
1441
|
+
const label = (t) => [t.padEnd(12), color.muted];
|
|
1442
|
+
const uses2 = (n2) => n2 ? `${n2} use${n2 === 1 ? "" : "s"}` : "unused";
|
|
1443
|
+
const skills = p.skills.map((s) => cursor || !w.days ? s.name : `${s.name} (${uses2(totalUses(w, pluginSkillKey(p, s.name)))})`);
|
|
1444
|
+
wrap(skills.join(" \xB7 ") || "none", width - 16, 4).forEach((l, i) => b.line(label(i ? "" : "Skills"), [l, color.text]));
|
|
1445
|
+
b.line(label("Also brings"), [p.parts.join(", ") || "nothing else", p.parts.length ? color.text : color.faint]);
|
|
1446
|
+
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`;
|
|
1447
|
+
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]);
|
|
1448
|
+
b.line(label("Folder"), [tildify3(p.path), color.faint]);
|
|
1449
|
+
return b.d;
|
|
1450
|
+
}
|
|
1451
|
+
};
|
|
1452
|
+
});
|
|
1127
1453
|
}
|
|
1128
1454
|
function addCursor(rows, cursor) {
|
|
1129
1455
|
const i = Math.min(cursor, rows.length - 1);
|
|
1130
1456
|
return rows[i] && !rows[i].header ? i : Math.max(0, rows.findIndex((r) => !r.header));
|
|
1131
1457
|
}
|
|
1132
|
-
|
|
1133
|
-
return typeof r === "string" ? r : r.message;
|
|
1134
|
-
}
|
|
1458
|
+
var said2 = said;
|
|
1135
1459
|
function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
1136
1460
|
const { exit, suspendTerminal } = useApp();
|
|
1137
1461
|
const { columns: termCols, rows: termRows } = useWindowSize();
|
|
@@ -1168,17 +1492,21 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1168
1492
|
const ui = {
|
|
1169
1493
|
notify: setToast,
|
|
1170
1494
|
openInGlobal,
|
|
1495
|
+
openInPlugins,
|
|
1171
1496
|
edit,
|
|
1172
|
-
copy: (text, what) => setToast(
|
|
1497
|
+
copy: (text, what) => setToast(said2(world.ops.copy(text, what))),
|
|
1173
1498
|
menu: (title, options) => setModal({ kind: "menu", title, options, cursor: 0 })
|
|
1174
1499
|
};
|
|
1500
|
+
const pluginIssues2 = new Map(world.plugins.map((p) => [p.key, pluginIssues(world, p)]));
|
|
1175
1501
|
const flagged = [
|
|
1176
1502
|
...world.projects.flatMap((p) => projectIssues(world, p.name).map((x) => ({ key: `${p.name}:${x.skill.name}`, issue: x.issue }))),
|
|
1177
|
-
...world.machine.flatMap((m) => machineIssues(world, m).map((issue) => ({ key: `Global:${m.name}`, issue })))
|
|
1503
|
+
...world.machine.flatMap((m) => machineIssues(world, m).map((issue) => ({ key: `Global:${m.name}`, issue }))),
|
|
1504
|
+
// The skilllib skill's issue is on its Global row when it has one (see agentSkillItems).
|
|
1505
|
+
...[agentSkillIssue(world)].flatMap((issue) => issue ? [{ key: "Global:skilllib", issue }] : [])
|
|
1178
1506
|
];
|
|
1179
1507
|
const healthCount = new Set(flagged.map((f) => f.key)).size;
|
|
1180
1508
|
const healthWorst = worst(flagged.map((f) => f.issue));
|
|
1181
|
-
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 === "health" ? healthItems(world, nameW, ui) : settingsItems();
|
|
1509
|
+
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();
|
|
1182
1510
|
const inChip = (i, c) => c === "all" || (c === "issues" ? i.issues.length > 0 : i.group === c);
|
|
1183
1511
|
let items = all.filter((i) => i.header || (!query || matches(i.name, query)) && inChip(i, chip));
|
|
1184
1512
|
const viewKey = [place, open, chip, query, used ? "usage" : ""].join("|");
|
|
@@ -1203,16 +1531,18 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1203
1531
|
if (items[cursor]?.header) cursor = firstSelectable;
|
|
1204
1532
|
const current = items[cursor];
|
|
1205
1533
|
const detail = current?.detail?.(columns);
|
|
1206
|
-
function apply(run) {
|
|
1534
|
+
function apply(run, w = world) {
|
|
1207
1535
|
let result;
|
|
1208
1536
|
try {
|
|
1209
|
-
result = run(
|
|
1537
|
+
result = run(w);
|
|
1210
1538
|
} catch (e) {
|
|
1211
|
-
result = `Couldn't do it: ${e.message}
|
|
1539
|
+
result = failed(`Couldn't do it: ${e.message}`);
|
|
1212
1540
|
}
|
|
1213
1541
|
setBase(reload());
|
|
1214
|
-
|
|
1215
|
-
|
|
1542
|
+
const then = followUp(result);
|
|
1543
|
+
const toast2 = `${isFailure(result) ? "\u2717" : then ? "!" : "\u2713"} ${said2(result)}`;
|
|
1544
|
+
setToast(toast2);
|
|
1545
|
+
if (then) setModal({ kind: "confirm", fix: then, toast: toast2 });
|
|
1216
1546
|
}
|
|
1217
1547
|
function choose(o) {
|
|
1218
1548
|
if (o.action) return o.action();
|
|
@@ -1232,6 +1562,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1232
1562
|
function fixSelected() {
|
|
1233
1563
|
const issue = current && worst(current.issues);
|
|
1234
1564
|
if (!issue) return setToast("Nothing to fix \xB7 enter shows what you can do");
|
|
1565
|
+
if (!issue.fixes.length) return setToast(`Nothing skilllib can do here: ${issue.short}`);
|
|
1235
1566
|
if (issue.decision) {
|
|
1236
1567
|
setFocus("detail");
|
|
1237
1568
|
setDetailCursor(Math.max(0, detail?.options.findIndex((o) => o.issue === issue) ?? 0));
|
|
@@ -1308,7 +1639,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1308
1639
|
preview: `Pick repos. These ${n(mine.length)} go into your library and those repos, and stop loading globally (originals backed up).`,
|
|
1309
1640
|
run: () => "",
|
|
1310
1641
|
preticked: usedIn,
|
|
1311
|
-
pickRepos: (w, repos) =>
|
|
1642
|
+
pickRepos: (w, repos) => w.ops.moveGlobal(mine, repos, label)
|
|
1312
1643
|
}),
|
|
1313
1644
|
...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`) })] : [],
|
|
1314
1645
|
fixOption({ label: `Delete all ${mine.length}`, preview: `Move these ${n(mine.length)} to Settings \u203A Backups.`, run: (w) => (mine.forEach((m) => w.ops.deleteGlobal(m)), `Deleted ${n(mine.length)} (in Settings \u203A Backups)`) })
|
|
@@ -1319,13 +1650,12 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1319
1650
|
preview: `Install these ${n(libs.length)} into the repos you pick.`,
|
|
1320
1651
|
run: () => "",
|
|
1321
1652
|
preticked: () => [],
|
|
1322
|
-
pickRepos: (w, repos) => (repos
|
|
1653
|
+
pickRepos: (w, repos) => w.ops.addTo(repos, libs.map((l) => l.name))
|
|
1323
1654
|
})
|
|
1324
1655
|
] : [],
|
|
1325
1656
|
...libs.length ? [fixOption(deleteLibraryFix(world, libs.map((l) => l.name)))] : [],
|
|
1326
1657
|
...locals.length ? [fixOption({ label: `Remove all ${locals.length} from this repo`, preview: `Remove these ${n(locals.length)} from ${open}; your library keeps the tracked ones.`, run: (w) => (locals.forEach((s) => w.ops.remove(open, s.name)), `Removed ${n(locals.length)} from ${open}`) })] : [],
|
|
1327
|
-
...[...new Set(machines.filter((m) => m.source === "plugin").map((m) => m.where))].map((id) =>
|
|
1328
|
-
...machines.some((m) => m.source === "plugin") ? [{ label: "Turn the plugin off with /plugin in Claude Code", action: () => setToast("Turn it off with /plugin in Claude Code; skilllib picks it up next time") }] : [],
|
|
1658
|
+
...[...new Set(machines.filter((m) => m.source === "plugin").map((m) => m.where))].map((id) => ({ label: `Open ${id.split("@")[0]} in Plugins`, action: () => openInPlugins(id) })),
|
|
1329
1659
|
{ label: "Review prompt", action: () => ui.copy(reviewPrompt(world, names, `Review these related skills (${label}).`), `a review prompt for ${label}`) }
|
|
1330
1660
|
];
|
|
1331
1661
|
}
|
|
@@ -1359,9 +1689,9 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1359
1689
|
`\u22EF ${repo}\u2026`,
|
|
1360
1690
|
["Open its folder, hide it from the list, or copy a review prompt for its skills."],
|
|
1361
1691
|
() => ui.menu(repo, [
|
|
1362
|
-
{ label: "Open its folder", action: () => setToast(
|
|
1692
|
+
{ label: "Open its folder", action: () => setToast(said2(world.ops.openFolder(project(world, repo).path))) },
|
|
1363
1693
|
{ label: "Hide it from the list", action: () => (apply((w) => w.ops.hide(repo)), setOpen(null), setFocus(sidebar ? "sidebar" : "list")) },
|
|
1364
|
-
...skills.length ? [{ label: `Review prompt for its ${skills.length} skills`, action: () => ui.copy(reviewPrompt(world, skills, `Review the skills in ${repo}
|
|
1694
|
+
...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}`) }] : []
|
|
1365
1695
|
])
|
|
1366
1696
|
)
|
|
1367
1697
|
];
|
|
@@ -1393,19 +1723,21 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1393
1723
|
setQuery("");
|
|
1394
1724
|
setChip("all");
|
|
1395
1725
|
}
|
|
1396
|
-
function edit(file) {
|
|
1397
|
-
if (!existsSync2(file)) return setToast(`${
|
|
1726
|
+
function edit(file, then) {
|
|
1727
|
+
if (!existsSync2(file)) return then ? apply(then) : (setBase(reload()), setToast(`${tildify3(file)} doesn't exist`));
|
|
1398
1728
|
const editor = process.env.VISUAL || process.env.EDITOR || (process.platform === "win32" ? "notepad" : "vi");
|
|
1399
1729
|
void suspendTerminal(() => {
|
|
1400
1730
|
if (process.platform === "win32") spawnSync2(`${editor} "${file}"`, { shell: true, stdio: "inherit" });
|
|
1401
1731
|
else spawnSync2("/bin/sh", ["-c", `${editor} "$1"`, "sh", file], { stdio: "inherit" });
|
|
1402
1732
|
}).then(() => {
|
|
1403
|
-
|
|
1404
|
-
|
|
1733
|
+
const fresh = reload();
|
|
1734
|
+
if (then) return apply(then, fresh);
|
|
1735
|
+
setBase(fresh);
|
|
1736
|
+
setToast(`Saved ${tildify3(file)}`);
|
|
1405
1737
|
});
|
|
1406
1738
|
}
|
|
1407
1739
|
function askForFolder(title) {
|
|
1408
|
-
setModal({ kind: "input", title, value: "~/Projects", submit: (v) => apply((w) => w.ops.addRoot(v)) });
|
|
1740
|
+
setModal({ kind: "input", title, value: "", placeholder: "~/Projects", submit: (v) => apply((w) => w.ops.addRoot(v)) });
|
|
1409
1741
|
}
|
|
1410
1742
|
useEffect(() => {
|
|
1411
1743
|
const folder = () => !initial.roots.length && askForFolder("Where are your projects? skilllib finds the git repos in there");
|
|
@@ -1416,6 +1748,10 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1416
1748
|
goPlace("global");
|
|
1417
1749
|
setQuery(name);
|
|
1418
1750
|
}
|
|
1751
|
+
function openInPlugins(id) {
|
|
1752
|
+
goPlace("plugins");
|
|
1753
|
+
setQuery(id);
|
|
1754
|
+
}
|
|
1419
1755
|
function settingsItems() {
|
|
1420
1756
|
const header = (key, text) => ({ key, name: "", header: true, issues: [], uses: 0, cells: [{ text }] });
|
|
1421
1757
|
const row = (key, text, sub, onEnter, c) => ({
|
|
@@ -1434,20 +1770,27 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1434
1770
|
apply((w) => (w.ops.setAgents(wasOn ? w.agents.filter((a) => a !== h.id) : HARNESSES.map((x) => x.id).filter((id) => id === h.id || w.agents.includes(id))), `${h.name} ${wasOn ? "off" : "on"}`));
|
|
1435
1771
|
})
|
|
1436
1772
|
),
|
|
1773
|
+
// The skill that lets your agents use skilllib: installing adds a global skill, so it asks first.
|
|
1774
|
+
row(
|
|
1775
|
+
"agent-skill",
|
|
1776
|
+
`${world.agentSkill === "installed" ? "[x]" : "[ ]"} \u25C6 skilllib skill`,
|
|
1777
|
+
world.agentSkill === "installed" ? "your agents can use skilllib" : world.agentSkill === "outdated" ? "out of date \xB7 enter updates it" : "lets your agents use skilllib \xB7 enter installs it",
|
|
1778
|
+
() => world.agentSkill === "installed" ? setToast("Installed \xB7 `skilllib agent-skill remove` takes it out") : choose(fixOption(agentSkillFix(world)))
|
|
1779
|
+
),
|
|
1437
1780
|
header("#roots", "Project folders"),
|
|
1438
1781
|
...world.roots.map(
|
|
1439
1782
|
(r) => row(
|
|
1440
1783
|
`root:${r}`,
|
|
1441
|
-
|
|
1784
|
+
tildify3(r),
|
|
1442
1785
|
"scanned for git repos",
|
|
1443
|
-
() => ui.menu(
|
|
1786
|
+
() => ui.menu(tildify3(r), [
|
|
1444
1787
|
{ label: "Look for repos again", action: () => apply((w) => w.ops.rescan()) },
|
|
1445
|
-
fixOption({ label: "Stop scanning this folder", preview: `Repos already found in ${
|
|
1788
|
+
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) })
|
|
1446
1789
|
])
|
|
1447
1790
|
)
|
|
1448
1791
|
),
|
|
1449
1792
|
row("root:add", "+ Add a folder\u2026", "", () => askForFolder("Folder that contains your projects")),
|
|
1450
|
-
...world.hidden.length ? [header("#hidden", "Hidden repos"), ...world.hidden.map((h) => row(`hidden:${h}`,
|
|
1793
|
+
...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))))] : [],
|
|
1451
1794
|
header("#backups", "Backups"),
|
|
1452
1795
|
...world.backups.length ? world.backups.map(
|
|
1453
1796
|
(b, i) => row(`backup:${i}`, `\u21BA ${b.name}`, `${b.from} \xB7 ${b.at}`, () => apply((w) => w.ops.restoreBackup(i)))
|
|
@@ -1551,7 +1894,11 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1551
1894
|
}
|
|
1552
1895
|
if (m.kind === "input") {
|
|
1553
1896
|
if (key.escape) return setModal(null);
|
|
1554
|
-
if (key.return)
|
|
1897
|
+
if (key.return) {
|
|
1898
|
+
const value = m.value.trim() || m.placeholder;
|
|
1899
|
+
setModal(null);
|
|
1900
|
+
return value ? m.submit(value) : void 0;
|
|
1901
|
+
}
|
|
1555
1902
|
if (key.backspace || key.delete) return setModal({ ...m, value: m.value.slice(0, -1) });
|
|
1556
1903
|
if (key.ctrl && input === "u") return setModal({ ...m, value: "" });
|
|
1557
1904
|
if (input && !key.ctrl && !key.meta) return setModal({ ...m, value: m.value + input });
|
|
@@ -1564,7 +1911,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1564
1911
|
if (key.return && m.options[m.cursor]) return setModal(null), choose(m.options[m.cursor]);
|
|
1565
1912
|
return;
|
|
1566
1913
|
}
|
|
1567
|
-
if (key.escape) return setModal(null);
|
|
1914
|
+
if (key.escape) return setModal(null), m.kind === "confirm" && m.toast && setToast(m.toast);
|
|
1568
1915
|
if (m.kind === "confirm") {
|
|
1569
1916
|
if (key.return || input === "y") {
|
|
1570
1917
|
setModal(null);
|
|
@@ -1601,17 +1948,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1601
1948
|
if (key.return) {
|
|
1602
1949
|
setModal(null);
|
|
1603
1950
|
const chosen = m.items.filter((it) => it.on);
|
|
1604
|
-
apply((w) =>
|
|
1605
|
-
let ok = 0;
|
|
1606
|
-
for (const it of chosen) {
|
|
1607
|
-
try {
|
|
1608
|
-
it.fix.run(w);
|
|
1609
|
-
ok++;
|
|
1610
|
-
} catch {
|
|
1611
|
-
}
|
|
1612
|
-
}
|
|
1613
|
-
return `Applied ${ok} fix${ok === 1 ? "" : "es"} (anything removed is in Settings \u203A Backups)`;
|
|
1614
|
-
});
|
|
1951
|
+
apply((w) => runAll(w, chosen.map((it) => it.fix)));
|
|
1615
1952
|
}
|
|
1616
1953
|
return;
|
|
1617
1954
|
}
|
|
@@ -1634,9 +1971,9 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1634
1971
|
const row = rows[cursor2];
|
|
1635
1972
|
if (row.create && !/^[a-z0-9]/.test(m.query)) return setToast("Type the new skill's name first");
|
|
1636
1973
|
setModal(null);
|
|
1637
|
-
apply(row.run);
|
|
1638
|
-
|
|
1639
|
-
return;
|
|
1974
|
+
if (!row.create) return apply(row.run);
|
|
1975
|
+
row.run(world);
|
|
1976
|
+
return edit(world.ops.libraryFile(m.query), row.afterEdit);
|
|
1640
1977
|
}
|
|
1641
1978
|
if (isNameChar(input, key)) return setModal({ ...m, query: m.query + input.toLowerCase(), cursor: 0 });
|
|
1642
1979
|
}
|
|
@@ -1648,6 +1985,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1648
1985
|
const globalFlagged = world.machine.map((m) => [m.name, machineIssues(world, m)]).filter(([, issues]) => issues.length);
|
|
1649
1986
|
const globalIssues = new Set(globalFlagged.map(([name]) => name)).size;
|
|
1650
1987
|
const globalProblem = globalFlagged.some(([, issues]) => issues.some((x) => x.severity === "problem"));
|
|
1988
|
+
const pluginsWorst = worst([...pluginIssues2.values()].flat().filter((i) => i.severity !== "hint"));
|
|
1651
1989
|
const badge = (p) => {
|
|
1652
1990
|
const found = projectIssues(world, p);
|
|
1653
1991
|
const top = worst(found.map((x) => x.issue));
|
|
@@ -1669,6 +2007,9 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1669
2007
|
sideRow("global", "\u25C8 Global", place === "global", () => goPlace("global"), [
|
|
1670
2008
|
{ text: String(new Set(world.machine.map((m) => m.name)).size), width: 4, color: globalProblem ? color.red : globalIssues ? color.yellow : color.muted }
|
|
1671
2009
|
]),
|
|
2010
|
+
sideRow("plugins", "\u29C9 Plugins", place === "plugins", () => goPlace("plugins"), [
|
|
2011
|
+
{ text: String(world.plugins.length), width: 4, color: pluginsWorst ? SEV[pluginsWorst.severity].color : color.muted }
|
|
2012
|
+
]),
|
|
1672
2013
|
sideRow("health", "\u2713 Health", place === "health", () => goPlace("health"), [
|
|
1673
2014
|
{ text: String(healthCount), width: 4, color: healthWorst ? SEV[healthWorst.severity].color : color.green }
|
|
1674
2015
|
]),
|
|
@@ -1706,17 +2047,17 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1706
2047
|
}),
|
|
1707
2048
|
/* @__PURE__ */ jsx2(Text2, { color: color.accent, children: query ? ` \u2315 ${query}\u258F` : "" })
|
|
1708
2049
|
] }) });
|
|
1709
|
-
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 === "health" ? titles([["", 2], ["Skill", nameW], ["Where", 16], [" Issue", 0]]) : null;
|
|
2050
|
+
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;
|
|
1710
2051
|
const showChips = chips.length > 0;
|
|
1711
2052
|
const listHeader = /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
1712
2053
|
showChips ? chipRow : null,
|
|
1713
2054
|
columnTitles ? /* @__PURE__ */ jsx2(TitleRow, { cells: columnTitles, width: columns }) : null
|
|
1714
2055
|
] });
|
|
1715
|
-
const overviewH = dashboard && !sidebar ?
|
|
2056
|
+
const overviewH = dashboard && !sidebar ? Math.ceil(overview(world, open).length / 2) + 2 : 0;
|
|
1716
2057
|
const listH = split ? Math.floor((bodyH - overviewH) / 2) : bodyH - overviewH;
|
|
1717
2058
|
const detailH = split ? bodyH - overviewH - listH : bodyH;
|
|
1718
2059
|
const listRows = items.map((i) => ({ key: i.key, cells: i.cells, header: i.header }));
|
|
1719
|
-
const listTitle = dashboard ? "Skills" : place === "projects" ? "Projects" : place === "library" ? "Your skills" : place === "global" ? "Loads everywhere" : place === "health" ? "To fix" : "Settings";
|
|
2060
|
+
const listTitle = dashboard ? "Skills" : place === "projects" ? "Projects" : place === "library" ? "Your skills" : place === "global" ? "Loads everywhere" : place === "plugins" ? "Plugins" : place === "health" ? "To fix" : "Settings";
|
|
1720
2061
|
const list = /* @__PURE__ */ jsx2(
|
|
1721
2062
|
ListPanel,
|
|
1722
2063
|
{
|
|
@@ -1749,8 +2090,11 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1749
2090
|
);
|
|
1750
2091
|
else if (modal?.kind === "input")
|
|
1751
2092
|
body = /* @__PURE__ */ jsxs2(Panel, { title: modal.title, focused: true, width: columns, height: 5, children: [
|
|
1752
|
-
/* @__PURE__ */ jsx2(Text2, { color: color.text, children: modal.value + "\u258F" }),
|
|
1753
|
-
|
|
2093
|
+
modal.value || !modal.placeholder ? /* @__PURE__ */ jsx2(Text2, { color: color.text, children: modal.value + "\u258F" }) : /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
2094
|
+
"\u258F",
|
|
2095
|
+
/* @__PURE__ */ jsx2(Text2, { color: color.faint, children: modal.placeholder })
|
|
2096
|
+
] }),
|
|
2097
|
+
/* @__PURE__ */ jsx2(Text2, { color: color.faint, children: modal.value || !modal.placeholder ? "enter to confirm \xB7 ctrl+u clears \xB7 esc cancels" : `type a path, or enter for ${modal.placeholder} \xB7 esc cancels` })
|
|
1754
2098
|
] });
|
|
1755
2099
|
else if (modal?.kind === "agents")
|
|
1756
2100
|
body = /* @__PURE__ */ jsx2(
|
|
@@ -1777,6 +2121,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1777
2121
|
else if (modal?.kind === "repos") {
|
|
1778
2122
|
const repos = (modal.only ?? world.projects.map((p) => p.name)).filter((r) => !modal.query || matches(r, modal.query));
|
|
1779
2123
|
const name = current?.name ?? "";
|
|
2124
|
+
const usedHere = (r) => current?.plugin ? pluginUses(world, current.plugin, r) : world.usage[r]?.[name] ?? 0;
|
|
1780
2125
|
const pickerLines = wrap(modal.fix.preview, columns - 6, 4);
|
|
1781
2126
|
body = /* @__PURE__ */ jsx2(
|
|
1782
2127
|
ListPanel,
|
|
@@ -1797,7 +2142,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1797
2142
|
cells: [
|
|
1798
2143
|
{ text: modal.picked.has(r) ? "[x] " : "[ ] ", width: 4, color: modal.picked.has(r) ? color.green : color.faint },
|
|
1799
2144
|
{ text: r, width: 20 },
|
|
1800
|
-
{ text:
|
|
2145
|
+
{ text: usedHere(r) ? `used ${usedHere(r)}\xD7 here` : "", grow: true, color: color.faint }
|
|
1801
2146
|
]
|
|
1802
2147
|
}))
|
|
1803
2148
|
}
|
|
@@ -1877,7 +2222,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1877
2222
|
] })
|
|
1878
2223
|
] }),
|
|
1879
2224
|
/* @__PURE__ */ jsxs2(Box2, { height: 1, width: termCols, justifyContent: "space-between", children: [
|
|
1880
|
-
/* @__PURE__ */ jsx2(Text2, { color: toast.startsWith("\u2713") || toast.startsWith("\u21BA") ? color.green : color.muted, wrap: "truncate-end", children: " " + toast }),
|
|
2225
|
+
/* @__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 }),
|
|
1881
2226
|
toast ? null : /* @__PURE__ */ jsx2(Text2, { color: color.faint, children: hints + " " })
|
|
1882
2227
|
] })
|
|
1883
2228
|
] });
|
|
@@ -1933,12 +2278,11 @@ function Help({ width, height }) {
|
|
|
1933
2278
|
}
|
|
1934
2279
|
|
|
1935
2280
|
// src/tui/load.ts
|
|
1936
|
-
import { execFileSync, spawn
|
|
2281
|
+
import { execFileSync, spawn } from "child_process";
|
|
1937
2282
|
import { existsSync as existsSync3, lstatSync, realpathSync } from "fs";
|
|
1938
2283
|
import { basename, dirname, join as join2 } from "path";
|
|
1939
|
-
function
|
|
1940
|
-
|
|
1941
|
-
return (p.startsWith(home) ? "~" + p.slice(home.length) : p).replace(/\\/g, "/");
|
|
2284
|
+
function tildify4(p) {
|
|
2285
|
+
return tildify(p).replace(/\\/g, "/");
|
|
1942
2286
|
}
|
|
1943
2287
|
function uniqueNames(roots) {
|
|
1944
2288
|
const tail = (r, n) => r.split(/[\\/]/).filter(Boolean).slice(-n).join("/");
|
|
@@ -1956,41 +2300,39 @@ function localSkill(root, s, git2) {
|
|
|
1956
2300
|
"update available": {},
|
|
1957
2301
|
"edited locally": { edited: true },
|
|
1958
2302
|
"edited locally, update available": { edited: true },
|
|
1959
|
-
|
|
2303
|
+
// sync restores it from your library, unless the library doesn't have it (a teammate's skill, another library).
|
|
2304
|
+
"folder missing": { missing: true, ...s.latest === null && { library: "missing" } },
|
|
1960
2305
|
"not in library": { library: "missing" }
|
|
1961
2306
|
};
|
|
1962
2307
|
const other = {
|
|
1963
2308
|
"untracked copy of library skill": { source: "untracked", library: "same" },
|
|
1964
2309
|
"untracked, differs from library": { source: "untracked", library: "differs" },
|
|
1965
2310
|
"local only": { source: "untracked" },
|
|
2311
|
+
// Installed with `npx skills add` (the repo's skills-lock.json): not skilllib's, so not a library skill.
|
|
2312
|
+
"from npx skills": { source: "untracked" },
|
|
1966
2313
|
"repo skill": { source: "repo" },
|
|
1967
2314
|
"repo skill, in library": { source: "repo", library: "same" },
|
|
1968
2315
|
"repo skill, differs from library": { source: "repo", library: "differs" }
|
|
1969
2316
|
};
|
|
2317
|
+
const state = git2 ? git2.of(relativeTo(root, s.path)) : null;
|
|
2318
|
+
const own = other[s.state]?.source === "untracked" && (state === "committed" || state === "changed") ? { source: "repo" } : {};
|
|
2319
|
+
const blind = s.state === "folder missing" ? [] : s.visibility.filter((v) => v.paths === 0).map((v) => v.id);
|
|
2320
|
+
const uncommitted = blind.length ? linkEverywhere(root, s.name, s.location, { dryRun: true }).uncommitted : [];
|
|
2321
|
+
const unshared = blind.filter((id) => uncommitted.includes(harness(id).projectDirs[0]));
|
|
1970
2322
|
return {
|
|
1971
2323
|
name: s.name,
|
|
1972
2324
|
source: "lib",
|
|
1973
2325
|
dir: s.location,
|
|
1974
2326
|
path: s.path,
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
2327
|
+
// Agents that actually load it here: visibility lists every agent you use, with `paths: 0` for the blind ones.
|
|
2328
|
+
agents: s.visibility.filter((v) => v.paths > 0).map((v) => v.id),
|
|
2329
|
+
...s.managed ? { library: "same", version: s.version ?? void 0, ...managed[s.state] } : { ...other[s.state], ...own },
|
|
2330
|
+
...unshared.length && { unshared },
|
|
2331
|
+
...s.source ? { origin: `npx skills: ${s.source}` } : {},
|
|
2332
|
+
added: addedAt(s.path),
|
|
2333
|
+
git: state
|
|
1978
2334
|
};
|
|
1979
2335
|
}
|
|
1980
|
-
var partsOf = /* @__PURE__ */ new Map();
|
|
1981
|
-
function pluginParts(root) {
|
|
1982
|
-
if (!partsOf.has(root))
|
|
1983
|
-
partsOf.set(
|
|
1984
|
-
root,
|
|
1985
|
-
[
|
|
1986
|
-
["commands", "commands"],
|
|
1987
|
-
["agents", "agents"],
|
|
1988
|
-
["hooks", "hooks"],
|
|
1989
|
-
[".mcp.json", "MCP servers"]
|
|
1990
|
-
].flatMap(([file, what]) => existsSync3(join2(root, file)) ? [what] : [])
|
|
1991
|
-
);
|
|
1992
|
-
return partsOf.get(root);
|
|
1993
|
-
}
|
|
1994
2336
|
function installedAt(path) {
|
|
1995
2337
|
try {
|
|
1996
2338
|
const st = lstatSync(path);
|
|
@@ -2004,30 +2346,51 @@ function machineSkill(s, kept) {
|
|
|
2004
2346
|
const global = s.kind === "global" || s.kind === "skills.sh";
|
|
2005
2347
|
return {
|
|
2006
2348
|
name: s.name,
|
|
2007
|
-
source: global ? "global" : s.kind === "plugin" ? "plugin" : s.kind === "claude.ai" ? "claude.ai" : s.kind === "system" ? "system" : "cursor",
|
|
2008
|
-
where: global ?
|
|
2349
|
+
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",
|
|
2350
|
+
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,
|
|
2009
2351
|
...s.kind === "skills.sh" ? { origin: `npx skills: ${s.origin}` } : {},
|
|
2010
2352
|
path: s.path,
|
|
2011
2353
|
links: s.links,
|
|
2012
2354
|
agents: s.harnesses,
|
|
2013
2355
|
...global && kept.has(s.name) ? { kept: true } : {},
|
|
2014
2356
|
...s.broken ? { broken: true } : {},
|
|
2015
|
-
...global && s.kind !== "skills.sh" ? { installed: installedAt(s.path) } : {}
|
|
2016
|
-
|
|
2357
|
+
...global && s.kind !== "skills.sh" ? { installed: installedAt(s.path) } : {}
|
|
2358
|
+
};
|
|
2359
|
+
}
|
|
2360
|
+
function pluginOf(p, repoOf) {
|
|
2361
|
+
const scope = p.agent === "cursor" ? "cursor" : p.synced ? "claude.ai" : p.scope ?? "user";
|
|
2362
|
+
const repo = p.projectPath ? repoOf(p.projectPath) ?? basename(p.projectPath) : void 0;
|
|
2363
|
+
return {
|
|
2364
|
+
key: [p.agent, p.id, scope, p.projectPath ?? ""].join("|"),
|
|
2365
|
+
id: p.id,
|
|
2366
|
+
agent: p.agent,
|
|
2367
|
+
scope,
|
|
2368
|
+
...repo ? { repo, projectPath: p.projectPath } : {},
|
|
2369
|
+
on: p.on,
|
|
2370
|
+
...p.version ? { version: p.version } : {},
|
|
2371
|
+
...p.update ? { update: p.update } : {},
|
|
2372
|
+
...p.settingsOnly ? { settingsOnly: true } : {},
|
|
2373
|
+
description: p.description,
|
|
2374
|
+
path: p.root,
|
|
2375
|
+
skills: p.skills.map((path) => ({ name: basename(path), path })),
|
|
2376
|
+
parts: p.extras
|
|
2017
2377
|
};
|
|
2018
2378
|
}
|
|
2379
|
+
function refOf(p) {
|
|
2380
|
+
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 } : {} };
|
|
2381
|
+
}
|
|
2019
2382
|
function dupesOf(r, root, git2 = null) {
|
|
2020
2383
|
const out = /* @__PURE__ */ new Map();
|
|
2021
2384
|
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)) });
|
|
2022
2385
|
for (const c of r.conflicts)
|
|
2023
|
-
out.set(c.name, { steps: [], git: [], differ: c.copies.map((x) => ({ dir: x.dir, label: root ? copyLabel(c, x.dir) :
|
|
2386
|
+
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 })) });
|
|
2024
2387
|
return out;
|
|
2025
2388
|
}
|
|
2026
2389
|
function loadWorld() {
|
|
2027
2390
|
forgetLatest();
|
|
2391
|
+
forgetClaudeBinary();
|
|
2028
2392
|
const agents = enabledHarnesses();
|
|
2029
|
-
const
|
|
2030
|
-
const here = isProjectCandidate(cwdRoot) && (existsSync3(join2(cwdRoot, ".git")) || existsSync3(join2(cwdRoot, "skilllib.json"))) ? cwdRoot : null;
|
|
2393
|
+
const here = projectHere();
|
|
2031
2394
|
const real = (p) => {
|
|
2032
2395
|
try {
|
|
2033
2396
|
return realpathSync(p);
|
|
@@ -2046,7 +2409,8 @@ function loadWorld() {
|
|
|
2046
2409
|
return { name: s.name, latest: latestVersion(s.name)?.version ?? 1, origin: origins[s.name] };
|
|
2047
2410
|
});
|
|
2048
2411
|
const kept = keptGlobal();
|
|
2049
|
-
const
|
|
2412
|
+
const cursorList = agents.includes("cursor") ? cursorPlugins() : [];
|
|
2413
|
+
const cursorPlugin = new Map(cursorPluginSkills(cursorList).map((s) => [s.name, s.plugin]));
|
|
2050
2414
|
const globalDupes = dupesOf(planGlobalTidy({ enabled: agents }), null);
|
|
2051
2415
|
const sourced = machineSkills(agents);
|
|
2052
2416
|
const machine = sourced.map((s) => {
|
|
@@ -2057,7 +2421,7 @@ function loadWorld() {
|
|
|
2057
2421
|
const plugin = cursorPlugin.get(m.name);
|
|
2058
2422
|
return { ...m, ...dupes ? { dupes } : {}, ...plugin ? { cursorPlugin: plugin } : {} };
|
|
2059
2423
|
});
|
|
2060
|
-
const
|
|
2424
|
+
const loadedHere = (root) => {
|
|
2061
2425
|
const loaded = skillsLoadedIn(root, sourced, agents);
|
|
2062
2426
|
if (loaded === sourced) return {};
|
|
2063
2427
|
for (const s of loaded) descriptions[s.name] ??= s.description;
|
|
@@ -2078,22 +2442,26 @@ function loadWorld() {
|
|
|
2078
2442
|
const plugin = cursorPlugin.get(s.name);
|
|
2079
2443
|
return { ...localSkill(root, s, git2), ...d ? { dupes: d } : {}, ...plugin ? { cursorPlugin: plugin } : {} };
|
|
2080
2444
|
}),
|
|
2081
|
-
...
|
|
2445
|
+
...loadedHere(root)
|
|
2082
2446
|
};
|
|
2083
2447
|
});
|
|
2084
2448
|
const backups = [...listBackups(), ...pluginBackups()].sort((a, b) => b.movedAt.localeCompare(a.movedAt));
|
|
2449
|
+
const repoOf = (path) => projects.find((p) => real(p.path) === real(path))?.name;
|
|
2450
|
+
const plugins = [...installedPlugins(), ...cursorList].map((p) => pluginOf(p, repoOf));
|
|
2085
2451
|
return {
|
|
2086
2452
|
agents,
|
|
2087
2453
|
cwd: cwd ? names.get(cwd) : null,
|
|
2088
2454
|
projects,
|
|
2089
2455
|
machine,
|
|
2456
|
+
plugins,
|
|
2090
2457
|
library,
|
|
2091
2458
|
usage: {},
|
|
2092
2459
|
descriptions,
|
|
2093
|
-
backups: backups.map((b) => ({ name: b.name, from:
|
|
2460
|
+
backups: backups.map((b) => ({ name: b.name, from: tildify4(b.from), at: b.movedAt })),
|
|
2094
2461
|
roots: readConfig().roots,
|
|
2095
2462
|
hidden: readConfig().hidden,
|
|
2096
2463
|
agentsChosen: harnessesChosen(),
|
|
2464
|
+
agentSkill: agentSkillState(agents),
|
|
2097
2465
|
ops: realOps(new Map(projects.map((p) => [p.name, p.path])), backups)
|
|
2098
2466
|
};
|
|
2099
2467
|
}
|
|
@@ -2125,44 +2493,101 @@ function remoteLabel(url) {
|
|
|
2125
2493
|
if (!url) return void 0;
|
|
2126
2494
|
return url.replace(/^[a-z+]+:\/\//, "").replace(/^[^@/]+@/, "").replace(/:(?!\d)/, "/").replace(/\.git$/, "");
|
|
2127
2495
|
}
|
|
2128
|
-
function
|
|
2496
|
+
function said3(c, repo) {
|
|
2129
2497
|
if (c.action === "skipped") return `${c.name}: ${c.reason ?? "skipped"}`;
|
|
2130
2498
|
if (c.action === "removed") return `Removed ${c.name} from ${repo}`;
|
|
2131
|
-
|
|
2499
|
+
const back = c.action === "updated" && c.from !== void 0 && c.to !== void 0 && c.to < c.from;
|
|
2500
|
+
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}`;
|
|
2501
|
+
return c.backedUp ? `${done} (your edits are in Settings \u203A Backups)` : done;
|
|
2132
2502
|
}
|
|
2133
2503
|
function realOps(roots, backups) {
|
|
2134
2504
|
const rootOf = (repo) => roots.get(repo);
|
|
2135
2505
|
const find = (repo, name) => projectStatus(rootOf(repo)).find((s) => s.name === name);
|
|
2136
2506
|
const info = /* @__PURE__ */ new Map();
|
|
2507
|
+
const result = (c, repo) => c.action === "skipped" ? failed(said3(c, repo)) : said3(c, repo);
|
|
2508
|
+
const copyDir = (repo, name) => readManifest(rootOf(repo)).skills[name]?.dir ?? PROJECT_SKILLS_DIR;
|
|
2137
2509
|
const withLinks = (c, repo, name) => {
|
|
2138
|
-
|
|
2139
|
-
|
|
2510
|
+
const it = (dirs) => dirs.length === 1 ? "it" : "them";
|
|
2511
|
+
if (c.uncommitted?.length)
|
|
2512
|
+
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`;
|
|
2513
|
+
if (!c.blocked?.length) return result(c, repo);
|
|
2514
|
+
return { message: `${said3(c, repo)}; not linked in ${c.blocked.join(", ")}: git tracks ${it(c.blocked)}`, then: allowTracked([{ repo, name, blocked: c.blocked }]) };
|
|
2140
2515
|
};
|
|
2141
|
-
const
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2516
|
+
const addTo = (repos, names) => {
|
|
2517
|
+
if (repos.length === 1 && names.length === 1) return withLinks(addSkill(rootOf(repos[0]), names[0]), repos[0], names[0]);
|
|
2518
|
+
return addMany(repos, names).result;
|
|
2519
|
+
};
|
|
2520
|
+
const addMany = (repos, names) => {
|
|
2521
|
+
const changes = repos.flatMap((repo) => names.map((name) => ({ repo, name, c: addSkill(rootOf(repo), name) })));
|
|
2522
|
+
const done = changes.filter((x) => x.c.action !== "skipped");
|
|
2523
|
+
const added = [...new Set(done.map((x) => x.name))];
|
|
2524
|
+
const summary = done.length ? `Added ${added.length === 1 ? `${added[0]} v${done[0].c.to}` : `${added.length} skills`} to ${[...new Set(done.map((x) => x.repo))].join(", ")}` : "";
|
|
2525
|
+
const notes = [];
|
|
2526
|
+
for (const repo of repos)
|
|
2527
|
+
for (const kind of ["blocked", "uncommitted"]) {
|
|
2528
|
+
const here = done.filter((x) => x.repo === repo && x.c[kind]?.length);
|
|
2529
|
+
if (!here.length) continue;
|
|
2530
|
+
const dirs = [...new Set(here.flatMap((x) => x.c[kind]))];
|
|
2531
|
+
const who = here.length === names.length ? "" : `${here.map((x) => x.name).join(", ")}: `;
|
|
2532
|
+
const copies = [...new Set(here.map((x) => copyDir(repo, x.name)))];
|
|
2533
|
+
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`;
|
|
2534
|
+
notes.push(`${who}not linked in ${dirs.join(", ")} in ${repo}: ${why}`);
|
|
2535
|
+
}
|
|
2536
|
+
const skipped = changes.filter((x) => x.c.action === "skipped").map((x) => `${x.name} not added to ${x.repo}: ${x.c.reason}`);
|
|
2537
|
+
const text = [summary, ...notes, ...skipped].filter(Boolean).join("; ");
|
|
2538
|
+
const held = done.flatMap((x) => x.c.blocked?.length ? [{ repo: x.repo, name: x.name, blocked: x.c.blocked }] : []);
|
|
2539
|
+
const result2 = !done.length ? failed(text) : held.length ? { message: text, then: allowTracked(held) } : skipped.length ? failed(text) : text;
|
|
2540
|
+
return { result: result2, skipped, notes };
|
|
2541
|
+
};
|
|
2542
|
+
const allowTracked = (held) => {
|
|
2543
|
+
const repos = [...new Set(held.map((h) => h.repo))];
|
|
2544
|
+
const dirs = [...new Set(held.flatMap((h) => h.blocked))];
|
|
2545
|
+
return {
|
|
2546
|
+
label: `Also link ${new Set(held.map((h) => h.name)).size === 1 ? "it" : "them"} in ${dirs.join(", ")}`,
|
|
2547
|
+
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(", ")}.`,
|
|
2548
|
+
run: (w) => {
|
|
2549
|
+
for (const repo of repos) allowTrackedLinks(rootOf(repo));
|
|
2550
|
+
if (held.length === 1) return w.ops.link(held[0].repo, held[0].name, true);
|
|
2551
|
+
const results = held.map((h) => ({ ...h, r: linkIn(h.repo, h.name, true) }));
|
|
2552
|
+
const linked = results.filter((x) => x.r.created.length);
|
|
2553
|
+
const names = [...new Set(linked.map((x) => x.name))];
|
|
2554
|
+
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(", ")}` : "";
|
|
2555
|
+
const others = results.filter((x) => !x.r.created.length || x.r.uncommitted.length).map((x) => `${x.repo}: ${linkSaid(x.name, x.r)}`);
|
|
2556
|
+
return [summary, ...others].join("; ");
|
|
2557
|
+
}
|
|
2558
|
+
};
|
|
2559
|
+
};
|
|
2560
|
+
const linkIn = (repo, name, allow) => {
|
|
2561
|
+
const s = find(repo, name);
|
|
2562
|
+
return s.managed ? relinkDependency(rootOf(repo), name, { allowTracked: allow }) : linkEverywhere(rootOf(repo), name, s.location, { allowTracked: allow });
|
|
2563
|
+
};
|
|
2564
|
+
const linkSaid = (name, r) => {
|
|
2565
|
+
const linked = r.created.length ? `linked in ${r.created.join(", ")}` : "";
|
|
2566
|
+
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` : "";
|
|
2567
|
+
return `${name}: ${[linked, notLinked].filter(Boolean).join("; ") || "nothing to link"}`;
|
|
2568
|
+
};
|
|
2569
|
+
const orForce = (c, repo, label, force) => c.action === "skipped" && c.reason?.includes("edits") ? {
|
|
2570
|
+
message: `${c.name} in ${repo} has local edits`,
|
|
2571
|
+
then: { label, preview: `Your edits to ${c.name} in ${repo} go to Settings \u203A Backups.`, run: () => result(force(), repo) }
|
|
2572
|
+
} : result(c, repo);
|
|
2150
2573
|
const toLibrary = (path) => {
|
|
2151
2574
|
const r = importSkill(path, { force: true });
|
|
2152
2575
|
return r.status === "unchanged" ? `${r.name} is already the same in your library` : `${r.name} v${latestVersion(r.name)?.version} is in your library`;
|
|
2153
2576
|
};
|
|
2154
2577
|
return {
|
|
2155
|
-
add: (repo, name) =>
|
|
2578
|
+
add: (repo, name) => addTo([repo], [name]),
|
|
2579
|
+
addTo,
|
|
2156
2580
|
remove: (repo, name) => {
|
|
2157
2581
|
const s = find(repo, name);
|
|
2158
|
-
if (!s) return `${name} isn't in ${repo}
|
|
2159
|
-
|
|
2582
|
+
if (!s) return failed(`${name} isn't in ${repo}`);
|
|
2583
|
+
const committed = !s.managed && ["committed", "changed"].includes(gitInfo(rootOf(repo))?.of(relativeTo(rootOf(repo), s.path)) ?? "");
|
|
2584
|
+
if (s.state.startsWith("repo skill") || committed) return failed(`${name} is the repo's own (committed by your team): skilllib doesn't delete it`);
|
|
2160
2585
|
if (!s.managed) return removeUntracked(rootOf(repo), name, s.path), `Removed ${name} from ${repo} (in Settings \u203A Backups)`;
|
|
2161
2586
|
return orForce(removeSkill(rootOf(repo), name), repo, "Remove it anyway", () => removeSkill(rootOf(repo), name, { force: true }));
|
|
2162
2587
|
},
|
|
2163
2588
|
restore: (repo, name) => {
|
|
2164
2589
|
const c = syncProject(rootOf(repo)).find((x) => x.name === name);
|
|
2165
|
-
return c ?
|
|
2590
|
+
return c ? result(c, repo) : `${name} is already in place`;
|
|
2166
2591
|
},
|
|
2167
2592
|
update: (repo, name) => {
|
|
2168
2593
|
const c = updateProject(rootOf(repo), [name])[0];
|
|
@@ -2180,10 +2605,10 @@ function realOps(roots, backups) {
|
|
|
2180
2605
|
return withLinks(addSkill(rootOf(repo), name, s.managed && s.version ? { version: s.version } : {}), repo, name);
|
|
2181
2606
|
},
|
|
2182
2607
|
link: (repo, name, allow = false) => {
|
|
2183
|
-
const
|
|
2184
|
-
const
|
|
2185
|
-
|
|
2186
|
-
return r.blocked.length
|
|
2608
|
+
const r = linkIn(repo, name, allow);
|
|
2609
|
+
const message = linkSaid(name, r);
|
|
2610
|
+
if (!r.blocked.length || allow) return r.created.length ? message : failed(message);
|
|
2611
|
+
return { message: `${message}; not linked in ${r.blocked.join(", ")}: git tracks ${r.blocked.length === 1 ? "it" : "them"}`, then: allowTracked([{ repo, name, blocked: r.blocked }]) };
|
|
2187
2612
|
},
|
|
2188
2613
|
track: (repo, name) => withLinks(addSkill(rootOf(repo), name), repo, name),
|
|
2189
2614
|
importLocal: (repo, name) => {
|
|
@@ -2199,10 +2624,9 @@ function realOps(roots, backups) {
|
|
|
2199
2624
|
if (!plan) return `${name}: nothing to tidy`;
|
|
2200
2625
|
const r = applyTidy(plan, { git: allowGit ? "go" : "keep" });
|
|
2201
2626
|
const steps = (list) => list.map((s) => describeStep(s, root)).join(", ");
|
|
2202
|
-
|
|
2203
|
-
if (!r.held.length) return message;
|
|
2627
|
+
if (!r.held.length) return r.applied.length ? `${name}: ${steps(r.applied)}` : `${name}: nothing changed`;
|
|
2204
2628
|
return {
|
|
2205
|
-
message,
|
|
2629
|
+
message: `${name}: ${r.applied.length ? `${steps(r.applied)}; ` : ""}held back ${steps(r.held)}, because git tracks it`,
|
|
2206
2630
|
then: {
|
|
2207
2631
|
label: "Also change what git tracks",
|
|
2208
2632
|
preview: `${steps(r.held)}: these changes will show in git status in ${repo}. Replaced copies go to Settings \u203A Backups.`,
|
|
@@ -2213,70 +2637,118 @@ function realOps(roots, backups) {
|
|
|
2213
2637
|
unloadGlobal: (m) => {
|
|
2214
2638
|
if (!m.broken && !latestVersion(m.name)) importSkill(m.path);
|
|
2215
2639
|
const r = unloadGlobal(m.path, m.links);
|
|
2216
|
-
return r.ok ? `${m.name} no longer loads globally (in Settings \u203A Backups)` : `${m.name}: ${r.reason}
|
|
2640
|
+
return r.ok ? `${m.name} no longer loads globally (in Settings \u203A Backups)` : failed(`${m.name}: ${r.reason}`);
|
|
2217
2641
|
},
|
|
2218
2642
|
deleteGlobal: (m) => {
|
|
2219
2643
|
const r = deleteGlobal(m.path, m.links);
|
|
2220
|
-
return r.ok ? m.broken ? `Removed the broken link ${m.name}` : `Deleted ${m.name} (in Settings \u203A Backups)` : `${m.name}: ${r.reason}
|
|
2644
|
+
return r.ok ? m.broken ? `Removed the broken link ${m.name}` : `Deleted ${m.name} (in Settings \u203A Backups)` : failed(`${m.name}: ${r.reason}`);
|
|
2221
2645
|
},
|
|
2222
2646
|
keepGlobal: (name, keep) => (setKeepGlobal([name], keep), keep ? `${name} marked as global on purpose` : `${name} is no longer marked as global on purpose`),
|
|
2223
2647
|
linkGlobal: (m, agents) => {
|
|
2224
2648
|
const r = linkGlobal(m.path, agents);
|
|
2225
|
-
const linked = r.linked.map(
|
|
2226
|
-
const skipped = r.skipped.length ? `; ${r.skipped.map(
|
|
2227
|
-
|
|
2649
|
+
const linked = r.linked.map(tildify4).join(", ");
|
|
2650
|
+
const skipped = r.skipped.length ? `; ${r.skipped.map(tildify4).join(", ")} already holds a different skill` : "";
|
|
2651
|
+
const message = `${m.name}: ${linked ? `linked ${linked}` : "nothing linked"}${skipped}`;
|
|
2652
|
+
return !linked && skipped ? failed(message) : message;
|
|
2228
2653
|
},
|
|
2229
|
-
moveGlobal: (
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
const
|
|
2233
|
-
const
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2654
|
+
moveGlobal: (skills, repos, group) => {
|
|
2655
|
+
const moved = [];
|
|
2656
|
+
const notes = [];
|
|
2657
|
+
const held = [];
|
|
2658
|
+
for (const m of skills) {
|
|
2659
|
+
if (importSkill(m.path).status === "exists") {
|
|
2660
|
+
notes.push(`${m.name}: your library has a different ${m.name}; nothing moved (update your library from this copy first)`);
|
|
2661
|
+
continue;
|
|
2662
|
+
}
|
|
2663
|
+
if (group) recordOrigin(m.name, `group: ${group}`);
|
|
2664
|
+
const results = repos.map((r2) => [r2, addSkill(rootOf(r2), m.name)]);
|
|
2665
|
+
const skipped = results.filter(([, c]) => c.action === "skipped");
|
|
2666
|
+
if (skipped.length) {
|
|
2667
|
+
notes.push(`${m.name}: not added to ${skipped.map(([r2, c]) => `${r2} (${c.reason})`).join(", ")}, so it still loads globally`);
|
|
2668
|
+
continue;
|
|
2669
|
+
}
|
|
2670
|
+
const r = unloadGlobal(m.path, m.links);
|
|
2671
|
+
if (!r.ok) {
|
|
2672
|
+
notes.push(`${m.name}: ${r.reason}`);
|
|
2673
|
+
continue;
|
|
2674
|
+
}
|
|
2675
|
+
moved.push(m.name);
|
|
2676
|
+
for (const [repo, c] of results) {
|
|
2677
|
+
const blind = visibilityOf(rootOf(repo), m.name).flatMap((v) => v.paths === 0 ? [v.id] : []);
|
|
2678
|
+
const blocked = [...c.blocked ?? [], ...c.uncommitted ?? []];
|
|
2679
|
+
if (!blocked.length || !blind.length) continue;
|
|
2680
|
+
held.push({ repo, name: m.name, blocked, blind, copy: copyDir(repo, m.name), shared: !c.uncommitted?.length });
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
const gaps = /* @__PURE__ */ new Map();
|
|
2684
|
+
for (const repo of new Set(held.map((h) => h.repo)))
|
|
2685
|
+
for (const shared of [true, false]) {
|
|
2686
|
+
const here = held.filter((h) => h.repo === repo && h.shared === shared);
|
|
2687
|
+
if (!here.length) continue;
|
|
2688
|
+
const agents = [...new Set(here.flatMap((h) => h.blind))].map((id) => harness(id).name).join(", ");
|
|
2689
|
+
const what = skills.length === 1 ? "it" : here.length === moved.length ? "them" : here.map((h) => h.name).join(", ");
|
|
2690
|
+
const dirs = [...new Set(here.flatMap((h) => h.blocked))].join(", ");
|
|
2691
|
+
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`;
|
|
2692
|
+
const key = `${agents}|${what}|${why}`;
|
|
2693
|
+
if (!gaps.has(key)) gaps.set(key, { who: `${agents} can't see ${what}`, why, repos: [] });
|
|
2694
|
+
gaps.get(key).repos.push(repo);
|
|
2695
|
+
}
|
|
2696
|
+
const blindNotes = [...gaps.values()].map((g) => `${g.who} in ${g.repos.join(", ")}: ${g.why}`);
|
|
2697
|
+
const summary = moved.length ? `${moved.length === 1 ? moved[0] : `${moved.length} skills`} now load${moved.length === 1 ? "s" : ""} only in ${repos.join(", ")}` : "";
|
|
2698
|
+
const text = [summary, ...blindNotes, ...notes].filter(Boolean).join("; ") || "Nothing moved";
|
|
2699
|
+
const askable = held.filter((h) => h.shared);
|
|
2700
|
+
if (!moved.length && notes.length) return failed(text);
|
|
2701
|
+
return askable.length ? { message: text, then: allowTracked(askable) } : text;
|
|
2240
2702
|
},
|
|
2241
2703
|
createSkill: (name) => {
|
|
2242
2704
|
const r = createSkill(name, "");
|
|
2243
|
-
return r.ok ? `Created ${name} in your library: edit its SKILL.md from Your skills` : `${name}: ${r.reason}
|
|
2705
|
+
return r.ok ? `Created ${name} in your library: edit its SKILL.md from Your skills` : failed(`${name}: ${r.reason}`);
|
|
2244
2706
|
},
|
|
2245
2707
|
pluginOffHere: (repo, id) => {
|
|
2246
2708
|
const root = rootOf(repo);
|
|
2247
2709
|
const plugin = claudePlugins(skillsLoadedIn(root, machineSkills()), root).find((p) => p.id === id);
|
|
2248
|
-
if (!plugin) return `${id} isn't on in ${repo}
|
|
2710
|
+
if (!plugin) return failed(`${id} isn't on in ${repo}`);
|
|
2249
2711
|
const r = turnOffIn(plugin, root);
|
|
2250
|
-
|
|
2251
|
-
return r.message;
|
|
2712
|
+
return r.ok ? r.message : failed(r.message);
|
|
2252
2713
|
},
|
|
2253
|
-
replacePlugin: (
|
|
2254
|
-
const
|
|
2255
|
-
const
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
}));
|
|
2261
|
-
|
|
2262
|
-
const copied = `${
|
|
2714
|
+
replacePlugin: (p, repos) => {
|
|
2715
|
+
const id = p.id;
|
|
2716
|
+
const imported = p.skills.map((s) => ({ name: s.name, status: importSkill(s.path).status }));
|
|
2717
|
+
const clashes = imported.filter((r) => r.status === "exists").map((r) => r.name);
|
|
2718
|
+
if (clashes.length) return failed(`${id} kept: your library has different ${clashes.join(", ")}; update or delete ${clashes.length === 1 ? "it" : "them"} first`);
|
|
2719
|
+
for (const r of imported) if (r.status !== "unchanged") recordOrigin(r.name, `plugin: ${id}`);
|
|
2720
|
+
const added = repos.length ? addMany(repos, p.skills.map((s) => s.name)) : { result: "", skipped: [], notes: [] };
|
|
2721
|
+
if (added.skipped.length) return failed(`${id} kept: ${added.skipped.join("; ")}`);
|
|
2722
|
+
const n = p.skills.length;
|
|
2723
|
+
const copied = `${n} skill${n === 1 ? " from " + id + " is" : "s from " + id + " are"} in your library${repos.length ? ` and in ${repos.join(", ")}` : ""}`;
|
|
2724
|
+
const notes = added.notes.length ? `; ${added.notes.join("; ")}` : "";
|
|
2725
|
+
const then = followUp(added.result);
|
|
2726
|
+
const done = (message) => then ? { message: message + notes, then } : message + notes;
|
|
2727
|
+
const ref = refOf(p);
|
|
2263
2728
|
const bin = claudeBinary();
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
return `${copied}; ${id} is uninstalled (restore it from Settings \u203A Backups, or reinstall with /plugin)`;
|
|
2729
|
+
let uninstallFailed = "";
|
|
2730
|
+
if (bin && !ref.synced && !ref.settingsOnly) {
|
|
2731
|
+
const r = uninstallPlugin(ref);
|
|
2732
|
+
if (r.ok) return done(`${copied}; ${r.message}`);
|
|
2733
|
+
uninstallFailed = ` (uninstalling failed: ${r.message.replace(`${id}: `, "")})`;
|
|
2270
2734
|
}
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
return
|
|
2735
|
+
const off = setPluginOn(ref, false);
|
|
2736
|
+
if (!off.ok) return failed(`${copied}; couldn't turn ${id} off (${off.message}): turn it off with /plugin${notes}`);
|
|
2737
|
+
if (ref.synced) return done(`${copied}; ${id} is off (it's synced from claude.ai: remove it there to delete it for good)`);
|
|
2738
|
+
if (ref.settingsOnly) return done(`${copied}; ${id} is off (it was only turned on in your settings, so there's nothing to uninstall)`);
|
|
2739
|
+
return done(`${copied}; ${id} is off${bin ? uninstallFailed : " (the claude command isn't on your PATH)"}: finish with /plugin uninstall ${id}`);
|
|
2740
|
+
},
|
|
2741
|
+
setPlugin: (p, on) => {
|
|
2742
|
+
const r = setPluginOn(refOf(p), on);
|
|
2743
|
+
return r.ok ? r.message : failed(r.message);
|
|
2744
|
+
},
|
|
2745
|
+
uninstallPlugin: (p) => {
|
|
2746
|
+
const r = uninstallPlugin(refOf(p));
|
|
2747
|
+
return r.ok ? r.message : failed(r.message);
|
|
2748
|
+
},
|
|
2749
|
+
updatePlugin: (p) => {
|
|
2750
|
+
const r = updatePlugin(refOf(p));
|
|
2751
|
+
return r.ok ? r.message : failed(r.message);
|
|
2280
2752
|
},
|
|
2281
2753
|
deleteLibrary: (name) => {
|
|
2282
2754
|
const from = [];
|
|
@@ -2287,13 +2759,13 @@ function realOps(roots, backups) {
|
|
|
2287
2759
|
from.push([...roots].find(([, r2]) => r2 === root)?.[0] ?? basename(root));
|
|
2288
2760
|
}
|
|
2289
2761
|
const r = deleteLibrarySkill(name);
|
|
2290
|
-
if (!r.ok) return `${name}: ${r.reason}
|
|
2762
|
+
if (!r.ok) return failed(`${name}: ${r.reason}`);
|
|
2291
2763
|
return `Deleted ${name} from your library${from.length ? ` and from ${from.join(", ")}` : ""} (in Settings \u203A Backups)`;
|
|
2292
2764
|
},
|
|
2293
2765
|
restoreBackup: (i) => {
|
|
2294
2766
|
const b = backups[i];
|
|
2295
2767
|
const r = b.kind === "plugin" ? restorePlugin(b) : restoreBackup(b);
|
|
2296
|
-
return r.ok ? `Restored ${b.name} to ${
|
|
2768
|
+
return r.ok ? `Restored ${b.name} to ${tildify4(r.to)}` : failed(`${b.name}: ${r.reason}`);
|
|
2297
2769
|
},
|
|
2298
2770
|
setAgents: (ids) => (setHarnesses(ids), "Saved your agents"),
|
|
2299
2771
|
installVersion: (repo, name, version) => withLinks(addSkill(rootOf(repo), name, { version, force: true }), repo, name),
|
|
@@ -2302,20 +2774,28 @@ function realOps(roots, backups) {
|
|
|
2302
2774
|
addRoot: (path) => {
|
|
2303
2775
|
const root = addRoot(path);
|
|
2304
2776
|
discoverProjects();
|
|
2305
|
-
return `Scanning ${
|
|
2777
|
+
return `Scanning ${tildify4(root)} for repos`;
|
|
2306
2778
|
},
|
|
2307
|
-
removeRoot: (path) => (removeRoot(path), `Stopped scanning ${
|
|
2779
|
+
removeRoot: (path) => (removeRoot(path), `Stopped scanning ${tildify4(path)}; repos already found stay until you hide them`),
|
|
2308
2780
|
rescan: () => (discoverProjects(), "Looked for repos in your project folders again"),
|
|
2309
2781
|
hide: (repo) => (setHidden(rootOf(repo), true), `Hid ${repo}: Settings \u203A Hidden repos brings it back`),
|
|
2310
|
-
unhide: (path) => (setHidden(path, false), `${
|
|
2782
|
+
unhide: (path) => (setHidden(path, false), `${tildify4(path)} is back in the list`),
|
|
2311
2783
|
openFolder: (path) => {
|
|
2312
2784
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
2313
2785
|
spawn(cmd, [path], { detached: true, stdio: "ignore" }).unref();
|
|
2314
|
-
return `Opened ${
|
|
2786
|
+
return `Opened ${tildify4(path)}`;
|
|
2315
2787
|
},
|
|
2316
2788
|
copy: (text, what) => {
|
|
2317
2789
|
const r = copyText(text);
|
|
2318
|
-
return r.copied ? `Copied ${what}: paste it into Claude Code, Cursor or
|
|
2790
|
+
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)}`;
|
|
2791
|
+
},
|
|
2792
|
+
installAgentSkill: () => {
|
|
2793
|
+
const r = installAgentSkill();
|
|
2794
|
+
return r.ok ? `Your agents can now use skilllib (${r.dirs.map(tildify4).join(", ")})` : failed(`skilllib skill: ${r.reason}`);
|
|
2795
|
+
},
|
|
2796
|
+
removeAgentSkill: () => {
|
|
2797
|
+
const removed = removeAgentSkill();
|
|
2798
|
+
return removed.length ? `Removed the skilllib skill (${removed.map(tildify4).join(", ")})` : failed("The skilllib skill isn't installed");
|
|
2319
2799
|
},
|
|
2320
2800
|
repoInfo: (repo) => {
|
|
2321
2801
|
if (!info.has(repo)) {
|