skilllib 1.7.0 → 1.11.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 +83 -35
- package/dist/{chunk-AYAN7Q2X.js → chunk-LO4FMXDE.js} +446 -148
- package/dist/index.js +12 -6
- package/dist/json.d.cts +222 -0
- package/dist/json.d.ts +222 -0
- package/dist/{tui-RU255WYX.js → tui-3LDZ5GOZ.js} +350 -107
- package/package.json +16 -3
- package/skills/skilllib/SKILL.md +12 -5
|
@@ -12,11 +12,13 @@ import {
|
|
|
12
12
|
copyLabel,
|
|
13
13
|
createSkill,
|
|
14
14
|
cursorPluginSkills,
|
|
15
|
+
cursorPlugins,
|
|
15
16
|
deleteGlobal,
|
|
16
17
|
deleteLibrarySkill,
|
|
17
18
|
describeStep,
|
|
18
19
|
discoverProjects,
|
|
19
20
|
enabledHarnesses,
|
|
21
|
+
forgetClaudeBinary,
|
|
20
22
|
forgetLatest,
|
|
21
23
|
gitInfo,
|
|
22
24
|
gitVisibleSteps,
|
|
@@ -24,6 +26,7 @@ import {
|
|
|
24
26
|
harnessesChosen,
|
|
25
27
|
importSkill,
|
|
26
28
|
installAgentSkill,
|
|
29
|
+
installedPlugins,
|
|
27
30
|
keptGlobal,
|
|
28
31
|
latestVersion,
|
|
29
32
|
libraryOrigins,
|
|
@@ -33,6 +36,7 @@ import {
|
|
|
33
36
|
linkGlobal,
|
|
34
37
|
listBackups,
|
|
35
38
|
machineSkills,
|
|
39
|
+
moveGlobalSkills,
|
|
36
40
|
planGlobalTidy,
|
|
37
41
|
planProjectTidy,
|
|
38
42
|
pluginBackups,
|
|
@@ -44,7 +48,6 @@ import {
|
|
|
44
48
|
readManifest,
|
|
45
49
|
readSkillInfo,
|
|
46
50
|
recordOrigin,
|
|
47
|
-
recordRemovedPlugin,
|
|
48
51
|
relativeTo,
|
|
49
52
|
relinkDependency,
|
|
50
53
|
removeAgentSkill,
|
|
@@ -57,18 +60,20 @@ import {
|
|
|
57
60
|
setHarnesses,
|
|
58
61
|
setHidden,
|
|
59
62
|
setKeepGlobal,
|
|
60
|
-
|
|
63
|
+
setPluginOn,
|
|
61
64
|
skilllibHome,
|
|
62
65
|
skillsLoadedIn,
|
|
63
66
|
syncProject,
|
|
64
67
|
tildify,
|
|
65
68
|
turnOffIn,
|
|
69
|
+
uninstallPlugin,
|
|
66
70
|
unloadGlobal,
|
|
71
|
+
updatePlugin,
|
|
67
72
|
updateProject,
|
|
68
73
|
versionHistory,
|
|
69
74
|
visibilityOf,
|
|
70
75
|
visibleProjects
|
|
71
|
-
} from "./chunk-
|
|
76
|
+
} from "./chunk-LO4FMXDE.js";
|
|
72
77
|
|
|
73
78
|
// src/tui/index.tsx
|
|
74
79
|
import { render } from "ink";
|
|
@@ -240,7 +245,7 @@ function buildReviewPrompt(skills, ctx) {
|
|
|
240
245
|
- 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.
|
|
241
246
|
- "Global" skills load in EVERY repo for the listed agents. Project skills only load inside that repo.
|
|
242
247
|
- I use these agents: ${ctx.harnesses.join(", ") || "(none configured)"}.
|
|
243
|
-
- 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.
|
|
248
|
+
- 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.
|
|
244
249
|
- 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.
|
|
245
250
|
|
|
246
251
|
## My projects
|
|
@@ -543,7 +548,7 @@ function issuesOf(w, projectName, u) {
|
|
|
543
548
|
title: `Same name as a skill in plugin ${vendor.where}: Claude Code has it off in ${projectName}, but Cursor ignores repo settings and lists both`,
|
|
544
549
|
short: "Cursor also lists a plugin's copy",
|
|
545
550
|
decision: true,
|
|
546
|
-
fixes: [
|
|
551
|
+
fixes: [pluginFixFor(w, vendor.where)]
|
|
547
552
|
});
|
|
548
553
|
else if (vendor)
|
|
549
554
|
issues.push({
|
|
@@ -745,25 +750,117 @@ function deleteLibraryFix(w, names) {
|
|
|
745
750
|
run: (w2) => joined(names.map((n) => w2.ops.deleteLibrary(n)))
|
|
746
751
|
};
|
|
747
752
|
}
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
+
var plural = (n, what) => `${n} ${what}${n === 1 ? "" : "s"}`;
|
|
754
|
+
function pluginName(p) {
|
|
755
|
+
return p.id.split("@")[0];
|
|
756
|
+
}
|
|
757
|
+
function pluginSkillKey(p, skill) {
|
|
758
|
+
return `${pluginName(p)}:${skill}`;
|
|
759
|
+
}
|
|
760
|
+
function pluginUses(w, p, repo) {
|
|
761
|
+
return p.skills.reduce((n, s) => n + (repo ? w.usage[repo]?.[pluginSkillKey(p, s.name)] ?? 0 : totalUses(w, pluginSkillKey(p, s.name))), 0);
|
|
762
|
+
}
|
|
763
|
+
function pluginScope(p) {
|
|
764
|
+
return p.repo ? `${p.scope} ${p.repo}` : p.scope;
|
|
765
|
+
}
|
|
766
|
+
function replacePluginFix(p) {
|
|
767
|
+
const names = p.skills.map((s) => s.name);
|
|
768
|
+
const synced = p.scope === "claude.ai";
|
|
769
|
+
const offOnly = synced || !!p.settingsOnly;
|
|
753
770
|
return {
|
|
754
|
-
label: `Replace ${
|
|
771
|
+
label: `Replace ${pluginName(p)} with library skills\u2026`,
|
|
755
772
|
preview: [
|
|
756
|
-
`Copies its ${
|
|
757
|
-
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
|
|
758
|
-
parts.length ? `It also brings ${parts.join(", ")}: those ${
|
|
759
|
-
|
|
773
|
+
`Copies its ${plural(names.length, "skill")} into your library and adds them to the repos you tick (or none),`,
|
|
774
|
+
synced ? `then turns ${p.id} off in Claude Code. It's synced from your claude.ai account: to delete it for good, remove it there.` : p.settingsOnly ? `then turns ${p.id} off in ~/.claude/settings.json. It isn't installed, only turned on there, so there's nothing to uninstall.` : `then uninstalls ${p.id}${p.repo ? ` from ${p.repo}` : ""} (its saved data is kept).`,
|
|
775
|
+
p.parts.length ? `It also brings ${p.parts.join(", ")}: those ${offOnly ? "stop" : "go"} too.` : "",
|
|
776
|
+
offOnly ? "Turn it back on from Plugins." : "Settings \u203A Backups reinstalls it."
|
|
760
777
|
].filter(Boolean).join(" "),
|
|
761
778
|
run: () => "",
|
|
762
779
|
allowNone: true,
|
|
763
|
-
preticked: (
|
|
764
|
-
pickRepos: (
|
|
780
|
+
preticked: (w) => w.projects.filter((x) => pluginUses(w, p, x.name) > 0).map((x) => x.name),
|
|
781
|
+
pickRepos: (w, repos) => w.ops.replacePlugin(p, repos)
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
function pluginFixFor(w, id) {
|
|
785
|
+
const ofId = w.plugins.filter((p2) => p2.agent === "claude-code" && p2.id === id);
|
|
786
|
+
const p = ofId.find((x) => !x.repo) ?? ofId[0];
|
|
787
|
+
if (p) return replacePluginFix(p);
|
|
788
|
+
return { label: "Turn the plugin off with /plugin in Claude Code", preview: `skilllib can't tell where ${id} is installed.`, run: () => `Turn ${id} off with /plugin in Claude Code; skilllib picks it up next time` };
|
|
789
|
+
}
|
|
790
|
+
function pluginSwitchFix(p, on) {
|
|
791
|
+
const where = p.repo ? ` in ${p.repo} (${p.scope} install)` : p.scope === "claude.ai" ? " on this machine" : " in every repo";
|
|
792
|
+
return {
|
|
793
|
+
label: on ? "Turn it on" : "Turn it off",
|
|
794
|
+
preview: `Turns ${p.id} ${on ? "on" : "off"}${where}${p.parts.length ? `, with its ${p.parts.join(", ")}` : ""}. ${p.scope === "claude.ai" ? `Sets "${p.id}": ${on} in ~/.claude/settings.json.` : `Runs claude plugin ${on ? "enable" : "disable"} ${p.id} --scope ${p.scope}${p.repo ? ` in ${p.repo}` : ""}.`} Claude Code picks it up in its next session.`,
|
|
795
|
+
run: (w) => w.ops.setPlugin(p, on)
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
function uninstallPluginFix(p) {
|
|
799
|
+
return {
|
|
800
|
+
label: "Uninstall",
|
|
801
|
+
preview: `Runs claude plugin uninstall ${p.id} --keep-data --scope ${p.scope}${p.repo ? ` in ${p.repo}` : ""}: its ${[plural(p.skills.length, "skill"), ...p.parts].join(", ")} go, its saved data stays. Settings \u203A Backups reinstalls it.`,
|
|
802
|
+
run: (w) => w.ops.uninstallPlugin(p)
|
|
765
803
|
};
|
|
766
804
|
}
|
|
805
|
+
function updatePluginFix(p) {
|
|
806
|
+
return {
|
|
807
|
+
label: p.update === "newer" ? "Update it" : `Update to ${p.update}`,
|
|
808
|
+
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.`,
|
|
809
|
+
run: (w) => w.ops.updatePlugin(p)
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
function pluginIssues(w, p) {
|
|
813
|
+
const library = new Set(w.library.map((l) => l.name));
|
|
814
|
+
const global = new Set(w.machine.filter((m) => m.source === "global" && !m.broken).map((m) => m.name));
|
|
815
|
+
if (p.agent === "cursor") {
|
|
816
|
+
const dupes = p.skills.filter((s) => library.has(s.name) || global.has(s.name));
|
|
817
|
+
return dupes.length ? [
|
|
818
|
+
{
|
|
819
|
+
id: `plugin-dupes:${p.key}`,
|
|
820
|
+
severity: "warning",
|
|
821
|
+
title: `Repeats your ${dupes.map((s) => s.name).join(", ")}: Cursor lists both while it's on`,
|
|
822
|
+
short: `Repeats ${dupes.length} of yours`,
|
|
823
|
+
decision: true,
|
|
824
|
+
fixes: [{ label: "Turn it off in Cursor (Settings \u203A Plugins)", preview: "skilllib can't read or change Cursor's plugins.", run: () => `Turn ${p.id} off in Cursor; skilllib can't tell whether it's on` }]
|
|
825
|
+
}
|
|
826
|
+
] : [];
|
|
827
|
+
}
|
|
828
|
+
const issues = [];
|
|
829
|
+
if (p.on) {
|
|
830
|
+
const repo = p.repo ? w.projects.find((x) => x.name === p.repo) : void 0;
|
|
831
|
+
const here = repo ? new Set(repo.skills.map((s) => s.name)) : void 0;
|
|
832
|
+
const yours = (n) => global.has(n) || (p.repo ? !!here?.has(n) : library.has(n));
|
|
833
|
+
const dupes = p.skills.filter((s) => yours(s.name));
|
|
834
|
+
if (dupes.length)
|
|
835
|
+
issues.push({
|
|
836
|
+
id: `plugin-dupes:${p.key}`,
|
|
837
|
+
severity: "warning",
|
|
838
|
+
title: `Repeats your ${dupes.map((s) => s.name).join(", ")}`,
|
|
839
|
+
short: `Repeats ${dupes.length} of yours`,
|
|
840
|
+
decision: true,
|
|
841
|
+
fixes: [replacePluginFix(p), pluginSwitchFix(p, false)]
|
|
842
|
+
});
|
|
843
|
+
if (w.days && p.skills.length && pluginUses(w, p) === 0)
|
|
844
|
+
issues.push({
|
|
845
|
+
id: `plugin-unused:${p.key}`,
|
|
846
|
+
severity: "hint",
|
|
847
|
+
title: `Its skills weren't used in 30 days (Claude Code)${p.parts.length ? `; it also brings ${p.parts.join(", ")}` : ""}`,
|
|
848
|
+
short: "Unused 30 days",
|
|
849
|
+
decision: true,
|
|
850
|
+
fixes: [pluginSwitchFix(p, false), ...p.scope === "claude.ai" || p.settingsOnly ? [] : [uninstallPluginFix(p)]]
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
if (p.update)
|
|
854
|
+
issues.push({
|
|
855
|
+
id: `plugin-update:${p.key}`,
|
|
856
|
+
severity: "hint",
|
|
857
|
+
title: p.update === "newer" ? "Its marketplace has a newer commit" : `Update available: ${p.version ?? "?"} \u2192 ${p.update}`,
|
|
858
|
+
short: p.update === "newer" ? "Update available" : `Update to ${p.update}`,
|
|
859
|
+
decision: true,
|
|
860
|
+
fixes: [updatePluginFix(p)]
|
|
861
|
+
});
|
|
862
|
+
return issues;
|
|
863
|
+
}
|
|
767
864
|
function originGroup(origin) {
|
|
768
865
|
if (!origin) return null;
|
|
769
866
|
const plugin = origin.match(/^plugin: (.+)$/);
|
|
@@ -777,17 +874,21 @@ function originGroup(origin) {
|
|
|
777
874
|
return null;
|
|
778
875
|
}
|
|
779
876
|
var word = (name) => name.split(/[-_.]/)[0];
|
|
877
|
+
function libraryOrigin(w, l) {
|
|
878
|
+
return l && (l.origin ?? w.library.find((x) => x.origin?.match(/^plugin: ([^@]+)@/)?.[1] === l.name)?.origin);
|
|
879
|
+
}
|
|
780
880
|
function groupCandidates(w, u) {
|
|
781
881
|
const byTime = (family, installed) => installed ? [{ key: `time:${family}:${installed}`, label: `installed together ${installed.slice(0, 10)}` }] : [];
|
|
782
882
|
const byWord = (family, name) => word(name).length >= 3 ? [{ key: `word:${family}:${word(name)}`, label: `${word(name)}-*` }] : [];
|
|
783
883
|
if (!("source" in u)) {
|
|
784
|
-
const origin = originGroup(u
|
|
884
|
+
const origin = originGroup(libraryOrigin(w, u));
|
|
785
885
|
return [...origin ? [origin] : [], ...byWord("library", u.name)];
|
|
786
886
|
}
|
|
787
887
|
const m = u.machine;
|
|
788
888
|
if (m?.source === "plugin") return [{ key: `plugin:${m.where}`, label: `\u29C9 ${m.where.split("@")[0]} plugin` }];
|
|
789
889
|
if (m?.source === "claude.ai") return [{ key: "claude.ai", label: "claude.ai skills" }];
|
|
790
890
|
if (m?.source === "cursor") return [{ key: "cursor", label: "Cursor built-ins" }];
|
|
891
|
+
if (m?.source === "grok") return [{ key: "grok", label: "Grok bundled" }];
|
|
791
892
|
if (m?.source === "system") return [{ key: `system:${m.where}`, label: m.where }];
|
|
792
893
|
if (m?.source === "skilllib") return [];
|
|
793
894
|
if (m) {
|
|
@@ -795,8 +896,7 @@ function groupCandidates(w, u) {
|
|
|
795
896
|
return [...origin ? [origin] : [], ...byTime("global", m.installed), ...byWord("global", m.name)];
|
|
796
897
|
}
|
|
797
898
|
if (u.local?.source === "lib") {
|
|
798
|
-
const
|
|
799
|
-
const origin = originGroup(l?.origin);
|
|
899
|
+
const origin = originGroup(libraryOrigin(w, w.library.find((x) => x.name === u.name)));
|
|
800
900
|
return [...origin ? [origin] : [], ...byWord("local", u.name)];
|
|
801
901
|
}
|
|
802
902
|
return byWord("local", u.name);
|
|
@@ -823,7 +923,33 @@ function addRows(w, target, q) {
|
|
|
823
923
|
const taken = new Set(target ? project(w, target).skills.map((s) => s.name) : []);
|
|
824
924
|
const loaded = new Set(target ? usable(w, target).map((u) => u.name) : []);
|
|
825
925
|
const hit = (name, description) => !q || matches(name, q) || q.length >= 3 && description.toLowerCase().includes(q);
|
|
826
|
-
const
|
|
926
|
+
const groups = assignGroups(w.library.map((l) => groupCandidates(w, l)));
|
|
927
|
+
const shown = w.library.map((l, i) => ({ l, g: groups[i] })).filter(({ l }) => !taken.has(l.name) && hit(l.name, w.descriptions[l.name] ?? ""));
|
|
928
|
+
const skillRow = (l, inGroup) => ({
|
|
929
|
+
key: `lib:${l.name}`,
|
|
930
|
+
name: l.name,
|
|
931
|
+
note: `v${l.latest}`,
|
|
932
|
+
description: w.descriptions[l.name] ?? "",
|
|
933
|
+
members: [l.name],
|
|
934
|
+
...inGroup ? { inGroup } : {},
|
|
935
|
+
run: (w2) => w2.ops.add(target, l.name)
|
|
936
|
+
});
|
|
937
|
+
const library = [];
|
|
938
|
+
const done = /* @__PURE__ */ new Set();
|
|
939
|
+
for (const { l, g } of target ? shown : []) {
|
|
940
|
+
if (done.has(l.name)) continue;
|
|
941
|
+
const members = g ? shown.filter((x) => x.g?.key === g.key).map((x) => x.l) : [l];
|
|
942
|
+
members.forEach((m) => done.add(m.name));
|
|
943
|
+
if (members.length < 2) {
|
|
944
|
+
library.push(skillRow(l, false));
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
const names = members.map((m) => m.name);
|
|
948
|
+
library.push(
|
|
949
|
+
{ key: `group:${g.key}`, name: groupLabel(g, names), note: `${names.length} skills`, description: names.join(", "), members: names, run: (w2) => w2.ops.addTo([target], names) },
|
|
950
|
+
...members.map((m) => skillRow(m, true))
|
|
951
|
+
);
|
|
952
|
+
}
|
|
827
953
|
const exists = q !== "" && (loaded.has(q) || w.library.some((l) => l.name === q));
|
|
828
954
|
const where = target ? ` for ${target}` : "";
|
|
829
955
|
const fresh = exists ? [] : [
|
|
@@ -918,6 +1044,7 @@ var PLACES = [
|
|
|
918
1044
|
["projects", "Projects"],
|
|
919
1045
|
["library", "Your skills"],
|
|
920
1046
|
["global", "Global"],
|
|
1047
|
+
["plugins", "Plugins"],
|
|
921
1048
|
["health", "Health"],
|
|
922
1049
|
["settings", "Settings"]
|
|
923
1050
|
];
|
|
@@ -996,7 +1123,7 @@ function TitleRow({ cells, width }) {
|
|
|
996
1123
|
}
|
|
997
1124
|
var fixOption = (fix) => ({ label: fix.label, fix });
|
|
998
1125
|
var GIT_LABEL = { committed: "\u2713 committed", changed: "\xB1 uncommitted changes", new: "+ not committed", ignored: "\u2205 gitignored" };
|
|
999
|
-
var COPY_RANK = { lib: 0, repo: 0, untracked: 0, global: 1, skilllib: 1, plugin: 2, "claude.ai": 3, cursor: 3, system: 3 };
|
|
1126
|
+
var COPY_RANK = { lib: 0, repo: 0, untracked: 0, global: 1, skilllib: 1, plugin: 2, "claude.ai": 3, cursor: 3, grok: 3, system: 3 };
|
|
1000
1127
|
function byName(copies) {
|
|
1001
1128
|
const groups = /* @__PURE__ */ new Map();
|
|
1002
1129
|
for (const c of copies) groups.set(c.name, [...groups.get(c.name) ?? [], c]);
|
|
@@ -1139,11 +1266,13 @@ function overview(w, p) {
|
|
|
1139
1266
|
const project2 = project(w, p);
|
|
1140
1267
|
const info = w.ops.repoInfo(p);
|
|
1141
1268
|
const manifest = project2.manifest === "none" ? ["no skilllib.json", color.faint] : project2.manifest === "no git" ? ["skilllib.json (not a git repo)", color.muted] : [`skilllib.json ${GIT_LABEL[project2.manifest]}`, project2.manifest === "committed" ? color.text : color.yellow];
|
|
1269
|
+
const plugins = [...new Set(machineIn(w, project2).filter((m) => m.source === "plugin").map((m) => m.where.split("@")[0]))];
|
|
1142
1270
|
return [
|
|
1143
1271
|
["Folder", [[tildify3(project2.path), color.text]]],
|
|
1144
1272
|
["Remote", [[info.remote ?? "none", info.remote ? color.blue : color.faint]]],
|
|
1145
1273
|
["Branch", [[info.branch ?? "\u2013", color.text], [info.dirty ? ` ${info.dirty} uncommitted` : "", color.yellow]]],
|
|
1146
|
-
["Manifest", [manifest]]
|
|
1274
|
+
["Manifest", [manifest]],
|
|
1275
|
+
["Plugins", [[plugins.length ? plugins.join(", ") : "none", plugins.length ? color.magenta : color.faint]]]
|
|
1147
1276
|
];
|
|
1148
1277
|
}
|
|
1149
1278
|
function InfoGrid({ rows, columns, width }) {
|
|
@@ -1185,13 +1314,21 @@ function globalItems(w, nameW, ui) {
|
|
|
1185
1314
|
const copies = w.machine.map((m) => ({ name: m.name, source: m.source, where: m.where, agents: m.agents, uses: totalUses(w, m.name), machine: m }));
|
|
1186
1315
|
return byName(copies).map((g) => skillItem(w, g, mergeIssues(g.map((c) => machineIssues(w, c.machine))), nameW, globalActions(w, g[0], ui), null));
|
|
1187
1316
|
}
|
|
1317
|
+
var AT_SOURCE = {
|
|
1318
|
+
global: null,
|
|
1319
|
+
plugin: null,
|
|
1320
|
+
skilllib: null,
|
|
1321
|
+
"claude.ai": "Turn it off in claude.ai \u203A Settings",
|
|
1322
|
+
cursor: "Cursor manages it",
|
|
1323
|
+
grok: "Ships with Grok; a same-named skill of yours overrides it"
|
|
1324
|
+
};
|
|
1188
1325
|
function globalActions(w, u, ui) {
|
|
1189
1326
|
const m = u.machine;
|
|
1190
|
-
const atSource = m.source === "
|
|
1327
|
+
const atSource = m.source === "system" ? `An admin manages ${m.where}` : AT_SOURCE[m.source];
|
|
1191
1328
|
return [
|
|
1192
1329
|
...machineActions(m).map(fixOption),
|
|
1193
|
-
...m.source === "plugin" ? [
|
|
1194
|
-
...
|
|
1330
|
+
...m.source === "plugin" ? [{ label: `Open ${m.where.split("@")[0]} in Plugins`, action: () => ui.openInPlugins(m.where) }] : [],
|
|
1331
|
+
...atSource ? [{ label: atSource, action: () => ui.notify(`${atSource}; skilllib picks up the change next time`) }] : [],
|
|
1195
1332
|
...m.source === "global" && !m.broken ? [{ label: "Edit SKILL.md", action: () => ui.edit(`${m.path}/SKILL.md`) }] : [],
|
|
1196
1333
|
reviewOption(w, m.name, ui)
|
|
1197
1334
|
];
|
|
@@ -1283,6 +1420,66 @@ function agentSkillItems(w, nameW) {
|
|
|
1283
1420
|
}
|
|
1284
1421
|
];
|
|
1285
1422
|
}
|
|
1423
|
+
var pluginW = (nameW) => Math.min(nameW, 17);
|
|
1424
|
+
var STATE = { on: ["on", color.green], off: ["off", color.faint], unknown: ["?", color.faint] };
|
|
1425
|
+
function pluginItems(w, skillW, ui, issuesOf2) {
|
|
1426
|
+
const nameW = pluginW(skillW);
|
|
1427
|
+
return w.plugins.map((p) => {
|
|
1428
|
+
const issues = issuesOf2.get(p.key) ?? [];
|
|
1429
|
+
const top = worst(issues);
|
|
1430
|
+
const cursor = p.agent === "cursor";
|
|
1431
|
+
const uses = cursor ? 0 : pluginUses(w, p);
|
|
1432
|
+
const [state, stateColor] = STATE[p.on === null ? "unknown" : p.on ? "on" : "off"];
|
|
1433
|
+
const n = p.skills.length;
|
|
1434
|
+
const name = pluginName(p);
|
|
1435
|
+
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`) }] : [
|
|
1436
|
+
fixOption(pluginSwitchFix(p, !p.on)),
|
|
1437
|
+
...p.update ? [fixOption(updatePluginFix(p))] : [],
|
|
1438
|
+
...n ? [fixOption(replacePluginFix(p))] : [],
|
|
1439
|
+
...p.scope === "claude.ai" || p.settingsOnly ? [] : [fixOption(uninstallPluginFix(p))]
|
|
1440
|
+
];
|
|
1441
|
+
return {
|
|
1442
|
+
key: p.key,
|
|
1443
|
+
name: p.id,
|
|
1444
|
+
plugin: p,
|
|
1445
|
+
issues,
|
|
1446
|
+
uses,
|
|
1447
|
+
cells: [
|
|
1448
|
+
{ text: top ? SEV[top.severity].icon : " ", width: 2, color: top ? SEV[top.severity].color : void 0 },
|
|
1449
|
+
{ text: clip(name, nameW), width: nameW, color: p.on === false ? color.muted : void 0 },
|
|
1450
|
+
{ text: harness(p.agent).icon, width: 3, color: harness(p.agent).color },
|
|
1451
|
+
{ text: clip(cursor ? "\u2013" : pluginScope(p), 19), width: 19, color: p.repo ? color.blue : color.muted },
|
|
1452
|
+
{ text: clip(`${n} skill${n === 1 ? "" : "s"}${p.parts.length ? ` +${p.parts.length}` : ""}`, 13), width: 13, color: color.muted },
|
|
1453
|
+
{ text: state.padEnd(4), width: 4, color: stateColor },
|
|
1454
|
+
{ text: cursor ? " \u2013 " : usesText(uses), width: 6, color: color.muted },
|
|
1455
|
+
{ text: " " + (top?.short ?? ""), grow: true, color: top ? SEV[top.severity].color : color.faint }
|
|
1456
|
+
],
|
|
1457
|
+
detail: (width) => {
|
|
1458
|
+
const b = builder(p.id);
|
|
1459
|
+
for (const l of wrap(p.description || "(no description)", width - 4, 2)) b.line([l, color.muted]);
|
|
1460
|
+
b.line();
|
|
1461
|
+
b.issues(issues);
|
|
1462
|
+
b.actions([...actions, { label: "Open its folder", action: () => ui.notify(said2(w.ops.openFolder(p.path))) }, {
|
|
1463
|
+
label: "Review prompt",
|
|
1464
|
+
action: () => ui.copy(
|
|
1465
|
+
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?`),
|
|
1466
|
+
`a review prompt for ${name}`
|
|
1467
|
+
)
|
|
1468
|
+
}], width);
|
|
1469
|
+
b.line();
|
|
1470
|
+
const label = (t) => [t.padEnd(12), color.muted];
|
|
1471
|
+
const uses2 = (n2) => n2 ? `${n2} use${n2 === 1 ? "" : "s"}` : "unused";
|
|
1472
|
+
const skills = p.skills.map((s) => cursor || !w.days ? s.name : `${s.name} (${uses2(totalUses(w, pluginSkillKey(p, s.name)))})`);
|
|
1473
|
+
wrap(skills.join(" \xB7 ") || "none", width - 16, 4).forEach((l, i) => b.line(label(i ? "" : "Skills"), [l, color.text]));
|
|
1474
|
+
b.line(label("Also brings"), [p.parts.join(", ") || "nothing else", p.parts.length ? color.text : color.faint]);
|
|
1475
|
+
const installed = cursor ? "in Cursor (whether it's on is only in Cursor)" : p.scope === "claude.ai" ? "synced from your claude.ai account" : p.repo ? `${p.scope} scope, in ${p.repo} only` : `user scope, in every repo`;
|
|
1476
|
+
b.line(label("Installed"), [installed, color.text], [p.version ? ` v${p.version.replace(/^v/, "")}` : "", color.muted], [p.id.includes("@") && p.scope !== "claude.ai" ? ` from ${p.id.split("@")[1]}` : "", color.faint]);
|
|
1477
|
+
b.line(label("Folder"), [tildify3(p.path), color.faint]);
|
|
1478
|
+
return b.d;
|
|
1479
|
+
}
|
|
1480
|
+
};
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1286
1483
|
function addCursor(rows, cursor) {
|
|
1287
1484
|
const i = Math.min(cursor, rows.length - 1);
|
|
1288
1485
|
return rows[i] && !rows[i].header ? i : Math.max(0, rows.findIndex((r) => !r.header));
|
|
@@ -1324,10 +1521,12 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1324
1521
|
const ui = {
|
|
1325
1522
|
notify: setToast,
|
|
1326
1523
|
openInGlobal,
|
|
1524
|
+
openInPlugins,
|
|
1327
1525
|
edit,
|
|
1328
1526
|
copy: (text, what) => setToast(said2(world.ops.copy(text, what))),
|
|
1329
1527
|
menu: (title, options) => setModal({ kind: "menu", title, options, cursor: 0 })
|
|
1330
1528
|
};
|
|
1529
|
+
const pluginIssues2 = new Map(world.plugins.map((p) => [p.key, pluginIssues(world, p)]));
|
|
1331
1530
|
const flagged = [
|
|
1332
1531
|
...world.projects.flatMap((p) => projectIssues(world, p.name).map((x) => ({ key: `${p.name}:${x.skill.name}`, issue: x.issue }))),
|
|
1333
1532
|
...world.machine.flatMap((m) => machineIssues(world, m).map((issue) => ({ key: `Global:${m.name}`, issue }))),
|
|
@@ -1336,7 +1535,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1336
1535
|
];
|
|
1337
1536
|
const healthCount = new Set(flagged.map((f) => f.key)).size;
|
|
1338
1537
|
const healthWorst = worst(flagged.map((f) => f.issue));
|
|
1339
|
-
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();
|
|
1538
|
+
const all = place === "projects" ? open ? projectSkillItems(world, open, nameW, ui) : projectItems(world) : place === "global" ? globalItems(world, nameW, ui) : place === "library" ? libraryItems(world, nameW, ui) : place === "plugins" ? pluginItems(world, nameW, ui, pluginIssues2) : place === "health" ? healthItems(world, nameW, ui) : settingsItems();
|
|
1340
1539
|
const inChip = (i, c) => c === "all" || (c === "issues" ? i.issues.length > 0 : i.group === c);
|
|
1341
1540
|
let items = all.filter((i) => i.header || (!query || matches(i.name, query)) && inChip(i, chip));
|
|
1342
1541
|
const viewKey = [place, open, chip, query, used ? "usage" : ""].join("|");
|
|
@@ -1485,8 +1684,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1485
1684
|
] : [],
|
|
1486
1685
|
...libs.length ? [fixOption(deleteLibraryFix(world, libs.map((l) => l.name)))] : [],
|
|
1487
1686
|
...locals.length ? [fixOption({ label: `Remove all ${locals.length} from this repo`, preview: `Remove these ${n(locals.length)} from ${open}; your library keeps the tracked ones.`, run: (w) => (locals.forEach((s) => w.ops.remove(open, s.name)), `Removed ${n(locals.length)} from ${open}`) })] : [],
|
|
1488
|
-
...[...new Set(machines.filter((m) => m.source === "plugin").map((m) => m.where))].map((id) =>
|
|
1489
|
-
...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") }] : [],
|
|
1687
|
+
...[...new Set(machines.filter((m) => m.source === "plugin").map((m) => m.where))].map((id) => ({ label: `Open ${id.split("@")[0]} in Plugins`, action: () => openInPlugins(id) })),
|
|
1490
1688
|
{ label: "Review prompt", action: () => ui.copy(reviewPrompt(world, names, `Review these related skills (${label}).`), `a review prompt for ${label}`) }
|
|
1491
1689
|
];
|
|
1492
1690
|
}
|
|
@@ -1513,7 +1711,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1513
1711
|
const repo = open;
|
|
1514
1712
|
const skills = project(world, repo).skills.map((s) => s.name);
|
|
1515
1713
|
return [
|
|
1516
|
-
row("#add", "+ Add a skill\u2026", ["Search your library, or create a new skill."], () => setModal({ kind: "add", cursor: 0, query: "", target: repo })),
|
|
1714
|
+
row("#add", "+ Add a skill\u2026", ["Search your library, or create a new skill."], () => setModal({ kind: "add", cursor: 0, query: "", target: repo, picked: /* @__PURE__ */ new Set() })),
|
|
1517
1715
|
...fix,
|
|
1518
1716
|
row(
|
|
1519
1717
|
"#repo",
|
|
@@ -1536,7 +1734,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1536
1734
|
}
|
|
1537
1735
|
if (place === "health") return fix;
|
|
1538
1736
|
if (place === "library")
|
|
1539
|
-
return [row("#add", "+ New skill\u2026", ["Create a skill in your library, or have an agent write it. Skills in your repos can be copied in from their own actions."], () => setModal({ kind: "add", cursor: 0, query: "", target: null }))];
|
|
1737
|
+
return [row("#add", "+ New skill\u2026", ["Create a skill in your library, or have an agent write it. Skills in your repos can be copied in from their own actions."], () => setModal({ kind: "add", cursor: 0, query: "", target: null, picked: /* @__PURE__ */ new Set() }))];
|
|
1540
1738
|
return [];
|
|
1541
1739
|
}
|
|
1542
1740
|
function move(delta) {
|
|
@@ -1579,6 +1777,10 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1579
1777
|
goPlace("global");
|
|
1580
1778
|
setQuery(name);
|
|
1581
1779
|
}
|
|
1780
|
+
function openInPlugins(id) {
|
|
1781
|
+
goPlace("plugins");
|
|
1782
|
+
setQuery(id);
|
|
1783
|
+
}
|
|
1582
1784
|
function settingsItems() {
|
|
1583
1785
|
const header = (key, text) => ({ key, name: "", header: true, issues: [], uses: 0, cells: [{ text }] });
|
|
1584
1786
|
const row = (key, text, sub, onEnter, c) => ({
|
|
@@ -1790,6 +1992,18 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1790
1992
|
if (key.upArrow) return step(-1);
|
|
1791
1993
|
if (key.downArrow) return step(1);
|
|
1792
1994
|
if (key.backspace || key.delete) return setModal({ ...m, query: m.query.slice(0, -1), cursor: 0 });
|
|
1995
|
+
const members = rows[cursor2]?.members;
|
|
1996
|
+
if (input === " " && members) {
|
|
1997
|
+
const picked = new Set(m.picked);
|
|
1998
|
+
const all2 = members.every((n) => picked.has(n));
|
|
1999
|
+
for (const n of members) all2 ? picked.delete(n) : picked.add(n);
|
|
2000
|
+
return setModal({ ...m, picked });
|
|
2001
|
+
}
|
|
2002
|
+
if (key.return && m.target && m.picked.size) {
|
|
2003
|
+
const [target, names] = [m.target, [...m.picked]];
|
|
2004
|
+
setModal(null);
|
|
2005
|
+
return apply((w) => w.ops.addTo([target], names));
|
|
2006
|
+
}
|
|
1793
2007
|
if (key.return && rows[cursor2]?.agent) {
|
|
1794
2008
|
setModal(null);
|
|
1795
2009
|
return ui.copy(writeSkillPrompt(world, m.query, m.target), "a prompt for an agent to write the skill");
|
|
@@ -1812,6 +2026,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1812
2026
|
const globalFlagged = world.machine.map((m) => [m.name, machineIssues(world, m)]).filter(([, issues]) => issues.length);
|
|
1813
2027
|
const globalIssues = new Set(globalFlagged.map(([name]) => name)).size;
|
|
1814
2028
|
const globalProblem = globalFlagged.some(([, issues]) => issues.some((x) => x.severity === "problem"));
|
|
2029
|
+
const pluginsWorst = worst([...pluginIssues2.values()].flat().filter((i) => i.severity !== "hint"));
|
|
1815
2030
|
const badge = (p) => {
|
|
1816
2031
|
const found = projectIssues(world, p);
|
|
1817
2032
|
const top = worst(found.map((x) => x.issue));
|
|
@@ -1833,6 +2048,9 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1833
2048
|
sideRow("global", "\u25C8 Global", place === "global", () => goPlace("global"), [
|
|
1834
2049
|
{ text: String(new Set(world.machine.map((m) => m.name)).size), width: 4, color: globalProblem ? color.red : globalIssues ? color.yellow : color.muted }
|
|
1835
2050
|
]),
|
|
2051
|
+
sideRow("plugins", "\u29C9 Plugins", place === "plugins", () => goPlace("plugins"), [
|
|
2052
|
+
{ text: String(world.plugins.length), width: 4, color: pluginsWorst ? SEV[pluginsWorst.severity].color : color.muted }
|
|
2053
|
+
]),
|
|
1836
2054
|
sideRow("health", "\u2713 Health", place === "health", () => goPlace("health"), [
|
|
1837
2055
|
{ text: String(healthCount), width: 4, color: healthWorst ? SEV[healthWorst.severity].color : color.green }
|
|
1838
2056
|
]),
|
|
@@ -1870,17 +2088,17 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1870
2088
|
}),
|
|
1871
2089
|
/* @__PURE__ */ jsx2(Text2, { color: color.accent, children: query ? ` \u2315 ${query}\u258F` : "" })
|
|
1872
2090
|
] }) });
|
|
1873
|
-
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;
|
|
2091
|
+
const columnTitles = dashboard || place === "global" ? titles([["", 2], ["Skill", nameW], ["Source", 14], [" Uses", 6], [" Issue", 0]]) : place === "projects" ? titles([["", 2], ["Project", 18], ["Skills", 10], ["Issues", 0]]) : place === "library" ? titles([["", 2], ["Skill", nameW], ["Ver", 5], ["Used in", 18], [" Uses", 6], [" Issue / description", 0]]) : place === "plugins" ? titles([["", 2], ["Plugin", pluginW(nameW)], ["", 3], ["Scope", 19], ["Brings", 13], ["On", 4], [" Uses", 6], [" Issue", 0]]) : place === "health" ? titles([["", 2], ["Skill", nameW], ["Where", 16], [" Issue", 0]]) : null;
|
|
1874
2092
|
const showChips = chips.length > 0;
|
|
1875
2093
|
const listHeader = /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
1876
2094
|
showChips ? chipRow : null,
|
|
1877
2095
|
columnTitles ? /* @__PURE__ */ jsx2(TitleRow, { cells: columnTitles, width: columns }) : null
|
|
1878
2096
|
] });
|
|
1879
|
-
const overviewH = dashboard && !sidebar ?
|
|
2097
|
+
const overviewH = dashboard && !sidebar ? Math.ceil(overview(world, open).length / 2) + 2 : 0;
|
|
1880
2098
|
const listH = split ? Math.floor((bodyH - overviewH) / 2) : bodyH - overviewH;
|
|
1881
2099
|
const detailH = split ? bodyH - overviewH - listH : bodyH;
|
|
1882
2100
|
const listRows = items.map((i) => ({ key: i.key, cells: i.cells, header: i.header }));
|
|
1883
|
-
const listTitle = dashboard ? "Skills" : place === "projects" ? "Projects" : place === "library" ? "Your skills" : place === "global" ? "Loads everywhere" : place === "health" ? "To fix" : "Settings";
|
|
2101
|
+
const listTitle = dashboard ? "Skills" : place === "projects" ? "Projects" : place === "library" ? "Your skills" : place === "global" ? "Loads everywhere" : place === "plugins" ? "Plugins" : place === "health" ? "To fix" : "Settings";
|
|
1884
2102
|
const list = /* @__PURE__ */ jsx2(
|
|
1885
2103
|
ListPanel,
|
|
1886
2104
|
{
|
|
@@ -1944,6 +2162,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1944
2162
|
else if (modal?.kind === "repos") {
|
|
1945
2163
|
const repos = (modal.only ?? world.projects.map((p) => p.name)).filter((r) => !modal.query || matches(r, modal.query));
|
|
1946
2164
|
const name = current?.name ?? "";
|
|
2165
|
+
const usedHere = (r) => current?.plugin ? pluginUses(world, current.plugin, r) : world.usage[r]?.[name] ?? 0;
|
|
1947
2166
|
const pickerLines = wrap(modal.fix.preview, columns - 6, 4);
|
|
1948
2167
|
body = /* @__PURE__ */ jsx2(
|
|
1949
2168
|
ListPanel,
|
|
@@ -1964,7 +2183,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1964
2183
|
cells: [
|
|
1965
2184
|
{ text: modal.picked.has(r) ? "[x] " : "[ ] ", width: 4, color: modal.picked.has(r) ? color.green : color.faint },
|
|
1966
2185
|
{ text: r, width: 20 },
|
|
1967
|
-
{ text:
|
|
2186
|
+
{ text: usedHere(r) ? `used ${usedHere(r)}\xD7 here` : "", grow: true, color: color.faint }
|
|
1968
2187
|
]
|
|
1969
2188
|
}))
|
|
1970
2189
|
}
|
|
@@ -1992,10 +2211,15 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
1992
2211
|
);
|
|
1993
2212
|
} else if (modal?.kind === "add") {
|
|
1994
2213
|
const rows = addRows(world, modal.target, modal.query);
|
|
2214
|
+
const picked = modal.picked;
|
|
2215
|
+
const tick = (names) => {
|
|
2216
|
+
const n = names.filter((x) => picked.has(x)).length;
|
|
2217
|
+
return { text: n === names.length ? "[x] " : n ? "[-] " : "[ ] ", width: 4, color: n ? color.green : color.faint };
|
|
2218
|
+
};
|
|
1995
2219
|
body = /* @__PURE__ */ jsx2(
|
|
1996
2220
|
ListPanel,
|
|
1997
2221
|
{
|
|
1998
|
-
title: modal.target ? `Add a skill to ${modal.target}` : "New skill",
|
|
2222
|
+
title: modal.target ? picked.size ? `Add ${picked.size} skill${picked.size === 1 ? "" : "s"} to ${modal.target}` : `Add a skill to ${modal.target}` : "New skill",
|
|
1999
2223
|
focused: true,
|
|
2000
2224
|
width: columns,
|
|
2001
2225
|
height: bodyH,
|
|
@@ -2004,10 +2228,14 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
2004
2228
|
header: /* @__PURE__ */ jsx2(Text2, { color: modal.query ? color.accent : color.muted, children: modal.query ? `\u2315 ${modal.query}\u258F` : modal.target ? "type to search your library, or to name a new skill" : "type the new skill's name" }),
|
|
2005
2229
|
headerHeight: 1,
|
|
2006
2230
|
rows: rows.map(
|
|
2007
|
-
(r) => r.header ? { key: r.key, header: true, cells: [{ text: r.header }] } : r.create || r.agent ? { key: r.key, cells: [{ text: r.name, grow: true, color: color.accent }] } : {
|
|
2231
|
+
(r) => r.header ? { key: r.key, header: true, cells: [{ text: r.header }] } : r.create || r.agent ? { key: r.key, cells: [{ text: r.name, grow: true, color: color.accent }] } : r.members.length > 1 ? {
|
|
2232
|
+
key: r.key,
|
|
2233
|
+
cells: [tick(r.members), { text: clip(r.name, 30), width: 30, bold: true }, { text: `${r.note}: ${r.description}`, grow: true, color: color.faint }]
|
|
2234
|
+
} : {
|
|
2008
2235
|
key: r.key,
|
|
2009
2236
|
cells: [
|
|
2010
|
-
|
|
2237
|
+
tick(r.members),
|
|
2238
|
+
{ text: clip(`${r.inGroup ? " " : ""}${r.name}`, 24), width: 24 },
|
|
2011
2239
|
{ text: r.note, width: 6, color: color.accent },
|
|
2012
2240
|
{ text: r.description, grow: true, color: color.faint }
|
|
2013
2241
|
]
|
|
@@ -2022,7 +2250,7 @@ function App({ initial, reload, loadUsage: loadUsage2 }) {
|
|
|
2022
2250
|
list,
|
|
2023
2251
|
split ? side : null
|
|
2024
2252
|
] });
|
|
2025
|
-
const hints = modal?.kind === "confirm" ? "enter apply \xB7 esc cancel" : modal?.kind === "repos" || modal?.kind === "fixall" ? "space tick \xB7 enter apply \xB7 esc cancel" : modal?.kind === "agents" ? "space tick \xB7 enter save" : modal?.kind === "menu" ? "\u2191\u2193 choose \xB7 enter do it \xB7 esc cancel" : modal?.kind === "add" ? "enter add \xB7 esc cancel" : modal ? "any key closes" : focus === "detail" ? "\u2191\u2193 \u2190\u2192 choose \xB7 enter apply \xB7 esc back" : focus === "sidebar" ? "\u2191\u2193 choose \xB7 enter open \xB7 type to filter repos" : place === "projects" && !open ? `type to filter \xB7 enter open \xB7 ${sidebar ? "\u2190 sidebar" : "\u2190\u2192 places"} \xB7 ? keys` : place === "settings" ? `enter toggle \xB7 ${sidebar ? "\u2190 sidebar" : "\u2190\u2192 places"} \xB7 ? keys` : `type to filter \xB7 space \u2605 fix \xB7 enter actions \xB7 tab tabs${sidebar ? " \xB7 esc back" : `${dashboard ? " \xB7 esc all projects" : ""} \xB7 ? keys`}`;
|
|
2253
|
+
const hints = modal?.kind === "confirm" ? "enter apply \xB7 esc cancel" : modal?.kind === "repos" || modal?.kind === "fixall" ? "space tick \xB7 enter apply \xB7 esc cancel" : modal?.kind === "agents" ? "space tick \xB7 enter save" : modal?.kind === "menu" ? "\u2191\u2193 choose \xB7 enter do it \xB7 esc cancel" : modal?.kind === "add" ? modal.target ? "space tick \xB7 enter add \xB7 esc cancel" : "enter add \xB7 esc cancel" : modal ? "any key closes" : focus === "detail" ? "\u2191\u2193 \u2190\u2192 choose \xB7 enter apply \xB7 esc back" : focus === "sidebar" ? "\u2191\u2193 choose \xB7 enter open \xB7 type to filter repos" : place === "projects" && !open ? `type to filter \xB7 enter open \xB7 ${sidebar ? "\u2190 sidebar" : "\u2190\u2192 places"} \xB7 ? keys` : place === "settings" ? `enter toggle \xB7 ${sidebar ? "\u2190 sidebar" : "\u2190\u2192 places"} \xB7 ? keys` : `type to filter \xB7 space \u2605 fix \xB7 enter actions \xB7 tab tabs${sidebar ? " \xB7 esc back" : `${dashboard ? " \xB7 esc all projects" : ""} \xB7 ? keys`}`;
|
|
2026
2254
|
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", width: termCols, height: termRows, children: [
|
|
2027
2255
|
sidebar ? null : topBar,
|
|
2028
2256
|
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
@@ -2100,7 +2328,7 @@ function Help({ width, height }) {
|
|
|
2100
2328
|
}
|
|
2101
2329
|
|
|
2102
2330
|
// src/tui/load.ts
|
|
2103
|
-
import { execFileSync, spawn
|
|
2331
|
+
import { execFileSync, spawn } from "child_process";
|
|
2104
2332
|
import { existsSync as existsSync3, lstatSync, realpathSync } from "fs";
|
|
2105
2333
|
import { basename, dirname, join as join2 } from "path";
|
|
2106
2334
|
function tildify4(p) {
|
|
@@ -2155,20 +2383,6 @@ function localSkill(root, s, git2) {
|
|
|
2155
2383
|
git: state
|
|
2156
2384
|
};
|
|
2157
2385
|
}
|
|
2158
|
-
var partsOf = /* @__PURE__ */ new Map();
|
|
2159
|
-
function pluginParts(root) {
|
|
2160
|
-
if (!partsOf.has(root))
|
|
2161
|
-
partsOf.set(
|
|
2162
|
-
root,
|
|
2163
|
-
[
|
|
2164
|
-
["commands", "commands"],
|
|
2165
|
-
["agents", "agents"],
|
|
2166
|
-
["hooks", "hooks"],
|
|
2167
|
-
[".mcp.json", "MCP servers"]
|
|
2168
|
-
].flatMap(([file, what]) => existsSync3(join2(root, file)) ? [what] : [])
|
|
2169
|
-
);
|
|
2170
|
-
return partsOf.get(root);
|
|
2171
|
-
}
|
|
2172
2386
|
function installedAt(path) {
|
|
2173
2387
|
try {
|
|
2174
2388
|
const st = lstatSync(path);
|
|
@@ -2182,7 +2396,7 @@ function machineSkill(s, kept) {
|
|
|
2182
2396
|
const global = s.kind === "global" || s.kind === "skills.sh";
|
|
2183
2397
|
return {
|
|
2184
2398
|
name: s.name,
|
|
2185
|
-
source: global ? "global" : s.kind === "plugin" ? "plugin" : s.kind === "claude.ai" ? "claude.ai" : s.kind === "system" ? "system" : s.kind === "skilllib" ? "skilllib" : "cursor",
|
|
2399
|
+
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",
|
|
2186
2400
|
where: global || s.kind === "skilllib" ? tildify4(dirname(s.path)) : s.kind === "claude.ai" ? "claude.ai account" : s.kind === "built-in" ? `${s.origin} built-in` : s.origin,
|
|
2187
2401
|
...s.kind === "skills.sh" ? { origin: `npx skills: ${s.origin}` } : {},
|
|
2188
2402
|
path: s.path,
|
|
@@ -2190,10 +2404,31 @@ function machineSkill(s, kept) {
|
|
|
2190
2404
|
agents: s.harnesses,
|
|
2191
2405
|
...global && kept.has(s.name) ? { kept: true } : {},
|
|
2192
2406
|
...s.broken ? { broken: true } : {},
|
|
2193
|
-
...global && s.kind !== "skills.sh" ? { installed: installedAt(s.path) } : {}
|
|
2194
|
-
|
|
2407
|
+
...global && s.kind !== "skills.sh" ? { installed: installedAt(s.path) } : {}
|
|
2408
|
+
};
|
|
2409
|
+
}
|
|
2410
|
+
function pluginOf(p, repoOf) {
|
|
2411
|
+
const scope = p.agent === "cursor" ? "cursor" : p.synced ? "claude.ai" : p.scope ?? "user";
|
|
2412
|
+
const repo = p.projectPath ? repoOf(p.projectPath) ?? basename(p.projectPath) : void 0;
|
|
2413
|
+
return {
|
|
2414
|
+
key: [p.agent, p.id, scope, p.projectPath ?? ""].join("|"),
|
|
2415
|
+
id: p.id,
|
|
2416
|
+
agent: p.agent,
|
|
2417
|
+
scope,
|
|
2418
|
+
...repo ? { repo, projectPath: p.projectPath } : {},
|
|
2419
|
+
on: p.on,
|
|
2420
|
+
...p.version ? { version: p.version } : {},
|
|
2421
|
+
...p.update ? { update: p.update } : {},
|
|
2422
|
+
...p.settingsOnly ? { settingsOnly: true } : {},
|
|
2423
|
+
description: p.description,
|
|
2424
|
+
path: p.root,
|
|
2425
|
+
skills: p.skills.map((path) => ({ name: basename(path), path })),
|
|
2426
|
+
parts: p.extras
|
|
2195
2427
|
};
|
|
2196
2428
|
}
|
|
2429
|
+
function refOf(p) {
|
|
2430
|
+
return { id: p.id, scope: p.scope === "claude.ai" ? null : p.scope, ...p.projectPath ? { projectPath: p.projectPath } : {}, synced: p.scope === "claude.ai", ...p.settingsOnly ? { settingsOnly: true } : {} };
|
|
2431
|
+
}
|
|
2197
2432
|
function dupesOf(r, root, git2 = null) {
|
|
2198
2433
|
const out = /* @__PURE__ */ new Map();
|
|
2199
2434
|
for (const p of r.plans) out.set(p.name, { steps: p.steps.map((s) => describeStep(s, root)), git: gitVisibleSteps(p, git2).map((s) => describeStep(s, root)) });
|
|
@@ -2203,6 +2438,7 @@ function dupesOf(r, root, git2 = null) {
|
|
|
2203
2438
|
}
|
|
2204
2439
|
function loadWorld() {
|
|
2205
2440
|
forgetLatest();
|
|
2441
|
+
forgetClaudeBinary();
|
|
2206
2442
|
const agents = enabledHarnesses();
|
|
2207
2443
|
const here = projectHere();
|
|
2208
2444
|
const real = (p) => {
|
|
@@ -2223,7 +2459,8 @@ function loadWorld() {
|
|
|
2223
2459
|
return { name: s.name, latest: latestVersion(s.name)?.version ?? 1, origin: origins[s.name] };
|
|
2224
2460
|
});
|
|
2225
2461
|
const kept = keptGlobal();
|
|
2226
|
-
const
|
|
2462
|
+
const cursorList = agents.includes("cursor") ? cursorPlugins() : [];
|
|
2463
|
+
const cursorPlugin = new Map(cursorPluginSkills(cursorList).map((s) => [s.name, s.plugin]));
|
|
2227
2464
|
const globalDupes = dupesOf(planGlobalTidy({ enabled: agents }), null);
|
|
2228
2465
|
const sourced = machineSkills(agents);
|
|
2229
2466
|
const machine = sourced.map((s) => {
|
|
@@ -2234,7 +2471,7 @@ function loadWorld() {
|
|
|
2234
2471
|
const plugin = cursorPlugin.get(m.name);
|
|
2235
2472
|
return { ...m, ...dupes ? { dupes } : {}, ...plugin ? { cursorPlugin: plugin } : {} };
|
|
2236
2473
|
});
|
|
2237
|
-
const
|
|
2474
|
+
const loadedHere = (root) => {
|
|
2238
2475
|
const loaded = skillsLoadedIn(root, sourced, agents);
|
|
2239
2476
|
if (loaded === sourced) return {};
|
|
2240
2477
|
for (const s of loaded) descriptions[s.name] ??= s.description;
|
|
@@ -2255,15 +2492,18 @@ function loadWorld() {
|
|
|
2255
2492
|
const plugin = cursorPlugin.get(s.name);
|
|
2256
2493
|
return { ...localSkill(root, s, git2), ...d ? { dupes: d } : {}, ...plugin ? { cursorPlugin: plugin } : {} };
|
|
2257
2494
|
}),
|
|
2258
|
-
...
|
|
2495
|
+
...loadedHere(root)
|
|
2259
2496
|
};
|
|
2260
2497
|
});
|
|
2261
2498
|
const backups = [...listBackups(), ...pluginBackups()].sort((a, b) => b.movedAt.localeCompare(a.movedAt));
|
|
2499
|
+
const repoOf = (path) => projects.find((p) => real(p.path) === real(path))?.name;
|
|
2500
|
+
const plugins = [...installedPlugins(), ...cursorList].map((p) => pluginOf(p, repoOf));
|
|
2262
2501
|
return {
|
|
2263
2502
|
agents,
|
|
2264
2503
|
cwd: cwd ? names.get(cwd) : null,
|
|
2265
2504
|
projects,
|
|
2266
2505
|
machine,
|
|
2506
|
+
plugins,
|
|
2267
2507
|
library,
|
|
2268
2508
|
usage: {},
|
|
2269
2509
|
descriptions,
|
|
@@ -2325,6 +2565,9 @@ function realOps(roots, backups) {
|
|
|
2325
2565
|
};
|
|
2326
2566
|
const addTo = (repos, names) => {
|
|
2327
2567
|
if (repos.length === 1 && names.length === 1) return withLinks(addSkill(rootOf(repos[0]), names[0]), repos[0], names[0]);
|
|
2568
|
+
return addMany(repos, names).result;
|
|
2569
|
+
};
|
|
2570
|
+
const addMany = (repos, names) => {
|
|
2328
2571
|
const changes = repos.flatMap((repo) => names.map((name) => ({ repo, name, c: addSkill(rootOf(repo), name) })));
|
|
2329
2572
|
const done = changes.filter((x) => x.c.action !== "skipped");
|
|
2330
2573
|
const added = [...new Set(done.map((x) => x.name))];
|
|
@@ -2343,9 +2586,8 @@ function realOps(roots, backups) {
|
|
|
2343
2586
|
const skipped = changes.filter((x) => x.c.action === "skipped").map((x) => `${x.name} not added to ${x.repo}: ${x.c.reason}`);
|
|
2344
2587
|
const text = [summary, ...notes, ...skipped].filter(Boolean).join("; ");
|
|
2345
2588
|
const held = done.flatMap((x) => x.c.blocked?.length ? [{ repo: x.repo, name: x.name, blocked: x.c.blocked }] : []);
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
return skipped.length ? failed(text) : text;
|
|
2589
|
+
const result2 = !done.length ? failed(text) : held.length ? { message: text, then: allowTracked(held) } : skipped.length ? failed(text) : text;
|
|
2590
|
+
return { result: result2, skipped, notes };
|
|
2349
2591
|
};
|
|
2350
2592
|
const allowTracked = (held) => {
|
|
2351
2593
|
const repos = [...new Set(held.map((h) => h.repo))];
|
|
@@ -2463,29 +2705,19 @@ function realOps(roots, backups) {
|
|
|
2463
2705
|
const moved = [];
|
|
2464
2706
|
const notes = [];
|
|
2465
2707
|
const held = [];
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
}
|
|
2471
|
-
if (group) recordOrigin(m.name, `group: ${group}`);
|
|
2472
|
-
const results = repos.map((r2) => [r2, addSkill(rootOf(r2), m.name)]);
|
|
2473
|
-
const skipped = results.filter(([, c]) => c.action === "skipped");
|
|
2474
|
-
if (skipped.length) {
|
|
2475
|
-
notes.push(`${m.name}: not added to ${skipped.map(([r2, c]) => `${r2} (${c.reason})`).join(", ")}, so it still loads globally`);
|
|
2476
|
-
continue;
|
|
2477
|
-
}
|
|
2478
|
-
const r = unloadGlobal(m.path, m.links);
|
|
2479
|
-
if (!r.ok) {
|
|
2480
|
-
notes.push(`${m.name}: ${r.reason}`);
|
|
2708
|
+
const repoRoots = repos.map(rootOf);
|
|
2709
|
+
for (const mv of moveGlobalSkills(skills, repoRoots, { group, label: (root) => repos[repoRoots.indexOf(root)] })) {
|
|
2710
|
+
if (!mv.moved) {
|
|
2711
|
+
notes.push(`${mv.name}: ${mv.reason}`);
|
|
2481
2712
|
continue;
|
|
2482
2713
|
}
|
|
2483
|
-
moved.push(
|
|
2484
|
-
for (const [
|
|
2485
|
-
const
|
|
2714
|
+
moved.push(mv.name);
|
|
2715
|
+
for (const [i, [root, c]] of mv.changes.entries()) {
|
|
2716
|
+
const repo = repos[i];
|
|
2717
|
+
const blind = visibilityOf(root, mv.name).flatMap((v) => v.paths === 0 ? [v.id] : []);
|
|
2486
2718
|
const blocked = [...c.blocked ?? [], ...c.uncommitted ?? []];
|
|
2487
2719
|
if (!blocked.length || !blind.length) continue;
|
|
2488
|
-
held.push({ repo, name:
|
|
2720
|
+
held.push({ repo, name: mv.name, blocked, blind, copy: copyDir(repo, mv.name), shared: !c.uncommitted?.length });
|
|
2489
2721
|
}
|
|
2490
2722
|
}
|
|
2491
2723
|
const gaps = /* @__PURE__ */ new Map();
|
|
@@ -2519,33 +2751,44 @@ function realOps(roots, backups) {
|
|
|
2519
2751
|
const r = turnOffIn(plugin, root);
|
|
2520
2752
|
return r.ok ? r.message : failed(r.message);
|
|
2521
2753
|
},
|
|
2522
|
-
replacePlugin: (
|
|
2523
|
-
const
|
|
2524
|
-
const
|
|
2754
|
+
replacePlugin: (p, repos) => {
|
|
2755
|
+
const id = p.id;
|
|
2756
|
+
const imported = p.skills.map((s) => ({ name: s.name, status: importSkill(s.path).status }));
|
|
2757
|
+
const clashes = imported.filter((r) => r.status === "exists").map((r) => r.name);
|
|
2525
2758
|
if (clashes.length) return failed(`${id} kept: your library has different ${clashes.join(", ")}; update or delete ${clashes.length === 1 ? "it" : "them"} first`);
|
|
2526
|
-
const
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
const
|
|
2759
|
+
for (const r of imported) if (r.status !== "unchanged") recordOrigin(r.name, `plugin: ${id}`);
|
|
2760
|
+
const added = repos.length ? addMany(repos, p.skills.map((s) => s.name)) : { result: "", skipped: [], notes: [] };
|
|
2761
|
+
if (added.skipped.length) return failed(`${id} kept: ${added.skipped.join("; ")}`);
|
|
2762
|
+
const n = p.skills.length;
|
|
2763
|
+
const copied = `${n} skill${n === 1 ? " from " + id + " is" : "s from " + id + " are"} in your library${repos.length ? ` and in ${repos.join(", ")}` : ""}`;
|
|
2764
|
+
const notes = added.notes.length ? `; ${added.notes.join("; ")}` : "";
|
|
2765
|
+
const then = followUp(added.result);
|
|
2766
|
+
const done = (message) => then ? { message: message + notes, then } : message + notes;
|
|
2767
|
+
const ref = refOf(p);
|
|
2532
2768
|
const bin = claudeBinary();
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
return `${copied}; ${id} is uninstalled (restore it from Settings \u203A Backups, or reinstall with /plugin)`;
|
|
2769
|
+
let uninstallFailed = "";
|
|
2770
|
+
if (bin && !ref.synced && !ref.settingsOnly) {
|
|
2771
|
+
const r = uninstallPlugin(ref);
|
|
2772
|
+
if (r.ok) return done(`${copied}; ${r.message}`);
|
|
2773
|
+
uninstallFailed = ` (uninstalling failed: ${r.message.replace(`${id}: `, "")})`;
|
|
2539
2774
|
}
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
return
|
|
2775
|
+
const off = setPluginOn(ref, false);
|
|
2776
|
+
if (!off.ok) return failed(`${copied}; couldn't turn ${id} off (${off.message}): turn it off with /plugin${notes}`);
|
|
2777
|
+
if (ref.synced) return done(`${copied}; ${id} is off (it's synced from claude.ai: remove it there to delete it for good)`);
|
|
2778
|
+
if (ref.settingsOnly) return done(`${copied}; ${id} is off (it was only turned on in your settings, so there's nothing to uninstall)`);
|
|
2779
|
+
return done(`${copied}; ${id} is off${bin ? uninstallFailed : " (the claude command isn't on your PATH)"}: finish with /plugin uninstall ${id}`);
|
|
2780
|
+
},
|
|
2781
|
+
setPlugin: (p, on) => {
|
|
2782
|
+
const r = setPluginOn(refOf(p), on);
|
|
2783
|
+
return r.ok ? r.message : failed(r.message);
|
|
2784
|
+
},
|
|
2785
|
+
uninstallPlugin: (p) => {
|
|
2786
|
+
const r = uninstallPlugin(refOf(p));
|
|
2787
|
+
return r.ok ? r.message : failed(r.message);
|
|
2788
|
+
},
|
|
2789
|
+
updatePlugin: (p) => {
|
|
2790
|
+
const r = updatePlugin(refOf(p));
|
|
2791
|
+
return r.ok ? r.message : failed(r.message);
|
|
2549
2792
|
},
|
|
2550
2793
|
deleteLibrary: (name) => {
|
|
2551
2794
|
const from = [];
|
|
@@ -2584,7 +2827,7 @@ function realOps(roots, backups) {
|
|
|
2584
2827
|
},
|
|
2585
2828
|
copy: (text, what) => {
|
|
2586
2829
|
const r = copyText(text);
|
|
2587
|
-
return r.copied ? `Copied ${what}: paste it into Claude Code, Cursor or
|
|
2830
|
+
return r.copied ? `Copied ${what}: paste it into Claude Code, Cursor, Codex or Grok` : `Couldn't reach the clipboard; ${what} is in ${tildify4(r.savedTo)}`;
|
|
2588
2831
|
},
|
|
2589
2832
|
installAgentSkill: () => {
|
|
2590
2833
|
const r = installAgentSkill();
|