claudeup 3.15.0 → 3.16.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/package.json +1 -1
- package/src/data/skill-repos.js +11 -0
- package/src/data/skill-repos.ts +12 -0
- package/src/services/skills-manager.js +40 -13
- package/src/services/skills-manager.ts +38 -16
- package/src/ui/adapters/skillsAdapter.js +5 -4
- package/src/ui/adapters/skillsAdapter.ts +5 -4
- package/src/ui/renderers/skillRenderers.js +12 -19
- package/src/ui/renderers/skillRenderers.tsx +46 -35
- package/src/ui/screens/SkillsScreen.js +5 -5
- package/src/ui/screens/SkillsScreen.tsx +4 -4
package/package.json
CHANGED
package/src/data/skill-repos.js
CHANGED
|
@@ -5,6 +5,7 @@ export const RECOMMENDED_SKILLS = [
|
|
|
5
5
|
skillPath: "skills/find-skills",
|
|
6
6
|
description: "Discover and install new skills from the ecosystem",
|
|
7
7
|
category: "search",
|
|
8
|
+
stars: 12000,
|
|
8
9
|
},
|
|
9
10
|
{
|
|
10
11
|
name: "React Best Practices",
|
|
@@ -12,6 +13,7 @@ export const RECOMMENDED_SKILLS = [
|
|
|
12
13
|
skillPath: "skills/react-best-practices",
|
|
13
14
|
description: "Modern React patterns and Vercel deployment guidelines",
|
|
14
15
|
category: "frontend",
|
|
16
|
+
stars: 24000,
|
|
15
17
|
},
|
|
16
18
|
{
|
|
17
19
|
name: "Web Design Guidelines",
|
|
@@ -19,6 +21,7 @@ export const RECOMMENDED_SKILLS = [
|
|
|
19
21
|
skillPath: "skills/web-design-guidelines",
|
|
20
22
|
description: "UI/UX design principles and web standards",
|
|
21
23
|
category: "design",
|
|
24
|
+
stars: 24000,
|
|
22
25
|
},
|
|
23
26
|
{
|
|
24
27
|
name: "Remotion Best Practices",
|
|
@@ -26,6 +29,7 @@ export const RECOMMENDED_SKILLS = [
|
|
|
26
29
|
skillPath: "skills/remotion-best-practices",
|
|
27
30
|
description: "Programmatic video creation with Remotion",
|
|
28
31
|
category: "media",
|
|
32
|
+
stars: 2400,
|
|
29
33
|
},
|
|
30
34
|
{
|
|
31
35
|
name: "UI/UX Pro Max",
|
|
@@ -33,6 +37,7 @@ export const RECOMMENDED_SKILLS = [
|
|
|
33
37
|
skillPath: "skills_en/ui-ux-pro-max",
|
|
34
38
|
description: "50 styles, 21 palettes, 50 font pairings, 9 stacks. Covers React, Next.js, Vue, Svelte, SwiftUI, Flutter, Tailwind, shadcn/ui",
|
|
35
39
|
category: "design",
|
|
40
|
+
stars: 109,
|
|
36
41
|
},
|
|
37
42
|
{
|
|
38
43
|
name: "ElevenLabs TTS",
|
|
@@ -40,6 +45,7 @@ export const RECOMMENDED_SKILLS = [
|
|
|
40
45
|
skillPath: "skills/elevenlabs-tts",
|
|
41
46
|
description: "Text-to-speech with ElevenLabs API integration",
|
|
42
47
|
category: "media",
|
|
48
|
+
stars: 206,
|
|
43
49
|
},
|
|
44
50
|
{
|
|
45
51
|
name: "Audit Website",
|
|
@@ -47,6 +53,7 @@ export const RECOMMENDED_SKILLS = [
|
|
|
47
53
|
skillPath: "skills/audit-website",
|
|
48
54
|
description: "Security and quality auditing for web applications",
|
|
49
55
|
category: "security",
|
|
56
|
+
stars: 67,
|
|
50
57
|
},
|
|
51
58
|
{
|
|
52
59
|
name: "Systematic Debugging",
|
|
@@ -54,6 +61,7 @@ export const RECOMMENDED_SKILLS = [
|
|
|
54
61
|
skillPath: "skills/systematic-debugging",
|
|
55
62
|
description: "Structured debugging methodology with root cause analysis",
|
|
56
63
|
category: "debugging",
|
|
64
|
+
stars: 113000,
|
|
57
65
|
},
|
|
58
66
|
{
|
|
59
67
|
name: "shadcn/ui",
|
|
@@ -61,6 +69,7 @@ export const RECOMMENDED_SKILLS = [
|
|
|
61
69
|
skillPath: "packages/shadcn",
|
|
62
70
|
description: "shadcn/ui component library patterns and usage",
|
|
63
71
|
category: "frontend",
|
|
72
|
+
stars: 111000,
|
|
64
73
|
},
|
|
65
74
|
{
|
|
66
75
|
name: "Neon Postgres",
|
|
@@ -68,6 +77,7 @@ export const RECOMMENDED_SKILLS = [
|
|
|
68
77
|
skillPath: "skills/neon-postgres",
|
|
69
78
|
description: "Neon serverless Postgres setup and best practices",
|
|
70
79
|
category: "database",
|
|
80
|
+
stars: 42,
|
|
71
81
|
},
|
|
72
82
|
{
|
|
73
83
|
name: "Neon Serverless",
|
|
@@ -75,6 +85,7 @@ export const RECOMMENDED_SKILLS = [
|
|
|
75
85
|
skillPath: "skills/neon-serverless",
|
|
76
86
|
description: "Serverless database patterns with Neon",
|
|
77
87
|
category: "database",
|
|
88
|
+
stars: 81,
|
|
78
89
|
},
|
|
79
90
|
];
|
|
80
91
|
export const DEFAULT_SKILL_REPOS = [
|
package/src/data/skill-repos.ts
CHANGED
|
@@ -6,6 +6,7 @@ export interface RecommendedSkill {
|
|
|
6
6
|
skillPath: string;
|
|
7
7
|
description: string;
|
|
8
8
|
category: string;
|
|
9
|
+
stars?: number; // fallback when GitHub API is rate-limited
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
|
|
@@ -15,6 +16,7 @@ export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
|
|
|
15
16
|
skillPath: "skills/find-skills",
|
|
16
17
|
description: "Discover and install new skills from the ecosystem",
|
|
17
18
|
category: "search",
|
|
19
|
+
stars: 12000,
|
|
18
20
|
},
|
|
19
21
|
{
|
|
20
22
|
name: "React Best Practices",
|
|
@@ -22,6 +24,7 @@ export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
|
|
|
22
24
|
skillPath: "skills/react-best-practices",
|
|
23
25
|
description: "Modern React patterns and Vercel deployment guidelines",
|
|
24
26
|
category: "frontend",
|
|
27
|
+
stars: 24000,
|
|
25
28
|
},
|
|
26
29
|
{
|
|
27
30
|
name: "Web Design Guidelines",
|
|
@@ -29,6 +32,7 @@ export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
|
|
|
29
32
|
skillPath: "skills/web-design-guidelines",
|
|
30
33
|
description: "UI/UX design principles and web standards",
|
|
31
34
|
category: "design",
|
|
35
|
+
stars: 24000,
|
|
32
36
|
},
|
|
33
37
|
{
|
|
34
38
|
name: "Remotion Best Practices",
|
|
@@ -36,6 +40,7 @@ export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
|
|
|
36
40
|
skillPath: "skills/remotion-best-practices",
|
|
37
41
|
description: "Programmatic video creation with Remotion",
|
|
38
42
|
category: "media",
|
|
43
|
+
stars: 2400,
|
|
39
44
|
},
|
|
40
45
|
{
|
|
41
46
|
name: "UI/UX Pro Max",
|
|
@@ -43,6 +48,7 @@ export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
|
|
|
43
48
|
skillPath: "skills_en/ui-ux-pro-max",
|
|
44
49
|
description: "50 styles, 21 palettes, 50 font pairings, 9 stacks. Covers React, Next.js, Vue, Svelte, SwiftUI, Flutter, Tailwind, shadcn/ui",
|
|
45
50
|
category: "design",
|
|
51
|
+
stars: 109,
|
|
46
52
|
},
|
|
47
53
|
{
|
|
48
54
|
name: "ElevenLabs TTS",
|
|
@@ -50,6 +56,7 @@ export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
|
|
|
50
56
|
skillPath: "skills/elevenlabs-tts",
|
|
51
57
|
description: "Text-to-speech with ElevenLabs API integration",
|
|
52
58
|
category: "media",
|
|
59
|
+
stars: 206,
|
|
53
60
|
},
|
|
54
61
|
{
|
|
55
62
|
name: "Audit Website",
|
|
@@ -57,6 +64,7 @@ export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
|
|
|
57
64
|
skillPath: "skills/audit-website",
|
|
58
65
|
description: "Security and quality auditing for web applications",
|
|
59
66
|
category: "security",
|
|
67
|
+
stars: 67,
|
|
60
68
|
},
|
|
61
69
|
{
|
|
62
70
|
name: "Systematic Debugging",
|
|
@@ -64,6 +72,7 @@ export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
|
|
|
64
72
|
skillPath: "skills/systematic-debugging",
|
|
65
73
|
description: "Structured debugging methodology with root cause analysis",
|
|
66
74
|
category: "debugging",
|
|
75
|
+
stars: 113000,
|
|
67
76
|
},
|
|
68
77
|
{
|
|
69
78
|
name: "shadcn/ui",
|
|
@@ -71,6 +80,7 @@ export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
|
|
|
71
80
|
skillPath: "packages/shadcn",
|
|
72
81
|
description: "shadcn/ui component library patterns and usage",
|
|
73
82
|
category: "frontend",
|
|
83
|
+
stars: 111000,
|
|
74
84
|
},
|
|
75
85
|
{
|
|
76
86
|
name: "Neon Postgres",
|
|
@@ -78,6 +88,7 @@ export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
|
|
|
78
88
|
skillPath: "skills/neon-postgres",
|
|
79
89
|
description: "Neon serverless Postgres setup and best practices",
|
|
80
90
|
category: "database",
|
|
91
|
+
stars: 42,
|
|
81
92
|
},
|
|
82
93
|
{
|
|
83
94
|
name: "Neon Serverless",
|
|
@@ -85,6 +96,7 @@ export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
|
|
|
85
96
|
skillPath: "skills/neon-serverless",
|
|
86
97
|
description: "Serverless database patterns with Neon",
|
|
87
98
|
category: "database",
|
|
99
|
+
stars: 81,
|
|
88
100
|
},
|
|
89
101
|
];
|
|
90
102
|
|
|
@@ -202,28 +202,55 @@ export async function fetchAvailableSkills(_repos, projectPath) {
|
|
|
202
202
|
// 2. Fetch popular skills from Firebase API
|
|
203
203
|
const popular = await fetchPopularSkills(30);
|
|
204
204
|
const popularSkills = popular.map((s) => markInstalled({ ...s, isRecommended: false }));
|
|
205
|
-
// 3. Enrich recommended skills with GitHub repo stars
|
|
206
|
-
|
|
205
|
+
// 3. Enrich recommended skills with GitHub repo stars (cached to disk)
|
|
206
|
+
const starsCachePath = path.join(os.homedir(), ".claude", "skill-stars-cache.json");
|
|
207
|
+
let starsCache = {};
|
|
208
|
+
try {
|
|
209
|
+
starsCache = await fs.readJson(starsCachePath);
|
|
210
|
+
}
|
|
211
|
+
catch { /* no cache yet */ }
|
|
207
212
|
const uniqueRepos = [...new Set(recommendedSkills.map((s) => s.source.repo))];
|
|
208
213
|
const repoStars = new Map();
|
|
209
|
-
|
|
210
|
-
|
|
214
|
+
const cacheMaxAge = 24 * 60 * 60 * 1000; // 24 hours
|
|
215
|
+
let cacheUpdated = false;
|
|
216
|
+
for (const repo of uniqueRepos) {
|
|
217
|
+
const cached = starsCache[repo];
|
|
218
|
+
if (cached && Date.now() - new Date(cached.fetchedAt).getTime() < cacheMaxAge) {
|
|
219
|
+
repoStars.set(repo, cached.stars);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
// Try fetching from GitHub (may be rate limited)
|
|
223
|
+
try {
|
|
211
224
|
const res = await fetch(`https://api.github.com/repos/${repo}`, {
|
|
212
225
|
headers: { Accept: "application/vnd.github+json" },
|
|
213
226
|
signal: AbortSignal.timeout(5000),
|
|
214
227
|
});
|
|
215
|
-
if (
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
228
|
+
if (res.ok) {
|
|
229
|
+
const data = (await res.json());
|
|
230
|
+
if (data.stargazers_count) {
|
|
231
|
+
repoStars.set(repo, data.stargazers_count);
|
|
232
|
+
starsCache[repo] = { stars: data.stargazers_count, fetchedAt: new Date().toISOString() };
|
|
233
|
+
cacheUpdated = true;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
else if (cached) {
|
|
237
|
+
// Rate limited but have stale cache — use it
|
|
238
|
+
repoStars.set(repo, cached.stars);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
if (cached)
|
|
243
|
+
repoStars.set(repo, cached.stars);
|
|
244
|
+
}
|
|
221
245
|
}
|
|
222
|
-
|
|
223
|
-
|
|
246
|
+
if (cacheUpdated) {
|
|
247
|
+
try {
|
|
248
|
+
await fs.writeJson(starsCachePath, starsCache);
|
|
249
|
+
}
|
|
250
|
+
catch { /* ignore */ }
|
|
224
251
|
}
|
|
225
252
|
for (const rec of recommendedSkills) {
|
|
226
|
-
rec.stars = repoStars.get(rec.source.repo) || undefined;
|
|
253
|
+
rec.stars = repoStars.get(rec.source.repo) || rec.stars || undefined;
|
|
227
254
|
}
|
|
228
255
|
// 4. Combine: recommended first, then popular (dedup by name)
|
|
229
256
|
const seen = new Set(recommendedSkills.map((s) => s.name));
|
|
@@ -271,27 +271,49 @@ export async function fetchAvailableSkills(
|
|
|
271
271
|
const popular = await fetchPopularSkills(30);
|
|
272
272
|
const popularSkills = popular.map((s) => markInstalled({ ...s, isRecommended: false }));
|
|
273
273
|
|
|
274
|
-
// 3. Enrich recommended skills with GitHub repo stars
|
|
275
|
-
|
|
274
|
+
// 3. Enrich recommended skills with GitHub repo stars (cached to disk)
|
|
275
|
+
const starsCachePath = path.join(os.homedir(), ".claude", "skill-stars-cache.json");
|
|
276
|
+
let starsCache: Record<string, { stars: number; fetchedAt: string }> = {};
|
|
277
|
+
try { starsCache = await fs.readJson(starsCachePath); } catch { /* no cache yet */ }
|
|
278
|
+
|
|
276
279
|
const uniqueRepos = [...new Set(recommendedSkills.map((s) => s.source.repo))];
|
|
277
280
|
const repoStars = new Map<string, number>();
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
281
|
+
const cacheMaxAge = 24 * 60 * 60 * 1000; // 24 hours
|
|
282
|
+
let cacheUpdated = false;
|
|
283
|
+
|
|
284
|
+
for (const repo of uniqueRepos) {
|
|
285
|
+
const cached = starsCache[repo];
|
|
286
|
+
if (cached && Date.now() - new Date(cached.fetchedAt).getTime() < cacheMaxAge) {
|
|
287
|
+
repoStars.set(repo, cached.stars);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
// Try fetching from GitHub (may be rate limited)
|
|
291
|
+
try {
|
|
292
|
+
const res = await fetch(`https://api.github.com/repos/${repo}`, {
|
|
293
|
+
headers: { Accept: "application/vnd.github+json" },
|
|
294
|
+
signal: AbortSignal.timeout(5000),
|
|
295
|
+
});
|
|
296
|
+
if (res.ok) {
|
|
286
297
|
const data = (await res.json()) as { stargazers_count?: number };
|
|
287
|
-
if (data.stargazers_count)
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
298
|
+
if (data.stargazers_count) {
|
|
299
|
+
repoStars.set(repo, data.stargazers_count);
|
|
300
|
+
starsCache[repo] = { stars: data.stargazers_count, fetchedAt: new Date().toISOString() };
|
|
301
|
+
cacheUpdated = true;
|
|
302
|
+
}
|
|
303
|
+
} else if (cached) {
|
|
304
|
+
// Rate limited but have stale cache — use it
|
|
305
|
+
repoStars.set(repo, cached.stars);
|
|
306
|
+
}
|
|
307
|
+
} catch {
|
|
308
|
+
if (cached) repoStars.set(repo, cached.stars);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (cacheUpdated) {
|
|
312
|
+
try { await fs.writeJson(starsCachePath, starsCache); } catch { /* ignore */ }
|
|
292
313
|
}
|
|
314
|
+
|
|
293
315
|
for (const rec of recommendedSkills) {
|
|
294
|
-
rec.stars = repoStars.get(rec.source.repo) || undefined;
|
|
316
|
+
rec.stars = repoStars.get(rec.source.repo) || rec.stars || undefined;
|
|
295
317
|
}
|
|
296
318
|
|
|
297
319
|
// 4. Combine: recommended first, then popular (dedup by name)
|
|
@@ -81,18 +81,19 @@ export function buildSkillBrowserItems({ recommended, popular, installed, search
|
|
|
81
81
|
}
|
|
82
82
|
return items;
|
|
83
83
|
}
|
|
84
|
-
// ── POPULAR (default, no search query) ──
|
|
85
|
-
|
|
84
|
+
// ── POPULAR (default, no search query) — only skills with meaningful stars ──
|
|
85
|
+
const popularWithStars = popular.filter((s) => (s.stars ?? 0) >= 5);
|
|
86
|
+
if (popularWithStars.length > 0) {
|
|
86
87
|
items.push({
|
|
87
88
|
id: "cat:popular",
|
|
88
89
|
kind: "category",
|
|
89
90
|
label: "Popular",
|
|
90
91
|
title: "Popular",
|
|
91
92
|
categoryKey: "popular",
|
|
92
|
-
count:
|
|
93
|
+
count: popularWithStars.length,
|
|
93
94
|
tone: "teal",
|
|
94
95
|
});
|
|
95
|
-
for (const skill of
|
|
96
|
+
for (const skill of popularWithStars) {
|
|
96
97
|
items.push({
|
|
97
98
|
id: `skill:${skill.id}`,
|
|
98
99
|
kind: "skill",
|
|
@@ -134,18 +134,19 @@ export function buildSkillBrowserItems({
|
|
|
134
134
|
return items;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
// ── POPULAR (default, no search query) ──
|
|
138
|
-
|
|
137
|
+
// ── POPULAR (default, no search query) — only skills with meaningful stars ──
|
|
138
|
+
const popularWithStars = popular.filter((s) => (s.stars ?? 0) >= 5);
|
|
139
|
+
if (popularWithStars.length > 0) {
|
|
139
140
|
items.push({
|
|
140
141
|
id: "cat:popular",
|
|
141
142
|
kind: "category",
|
|
142
143
|
label: "Popular",
|
|
143
144
|
title: "Popular",
|
|
144
145
|
categoryKey: "popular",
|
|
145
|
-
count:
|
|
146
|
+
count: popularWithStars.length,
|
|
146
147
|
tone: "teal",
|
|
147
148
|
});
|
|
148
|
-
for (const skill of
|
|
149
|
+
for (const skill of popularWithStars) {
|
|
149
150
|
items.push({
|
|
150
151
|
id: `skill:${skill.id}`,
|
|
151
152
|
kind: "skill",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
-
import { SelectableRow, ListCategoryRow, ScopeSquares,
|
|
2
|
+
import { SelectableRow, ListCategoryRow, ScopeSquares, MetaText, KeyValueLine, DetailSection, } from "../components/primitives/index.js";
|
|
3
3
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
4
4
|
function formatStars(stars) {
|
|
5
5
|
if (!stars)
|
|
@@ -26,36 +26,29 @@ const categoryRenderer = {
|
|
|
26
26
|
},
|
|
27
27
|
};
|
|
28
28
|
// ─── Skill renderer ───────────────────────────────────────────────────────────
|
|
29
|
+
const MAX_SKILL_NAME_LEN = 35;
|
|
30
|
+
function truncateName(name) {
|
|
31
|
+
return name.length > MAX_SKILL_NAME_LEN
|
|
32
|
+
? name.slice(0, MAX_SKILL_NAME_LEN - 1) + "\u2026"
|
|
33
|
+
: name;
|
|
34
|
+
}
|
|
29
35
|
const skillRenderer = {
|
|
30
36
|
renderRow: ({ item, isSelected }) => {
|
|
31
37
|
const { skill } = item;
|
|
32
38
|
const hasUser = skill.installedScope === "user";
|
|
33
39
|
const hasProject = skill.installedScope === "project";
|
|
34
40
|
const starsStr = formatStars(skill.stars);
|
|
35
|
-
|
|
41
|
+
const displayName = truncateName(skill.name);
|
|
42
|
+
return (_jsxs(SelectableRow, { selected: isSelected, indent: 1, children: [_jsx(ScopeSquares, { user: hasUser, project: hasProject, selected: isSelected }), _jsx("span", { children: " " }), _jsx("span", { fg: isSelected ? "white" : skill.installed ? "white" : "gray", children: displayName }), skill.hasUpdate ? _jsx(MetaText, { text: " \u2B06", tone: "warning" }) : null, starsStr ? _jsx(MetaText, { text: ` ${starsStr}`, tone: "warning" }) : null] }));
|
|
36
43
|
},
|
|
37
44
|
renderDetail: ({ item }) => {
|
|
38
45
|
const { skill } = item;
|
|
39
46
|
const fm = skill.frontmatter;
|
|
40
47
|
const description = fm?.description || skill.description || "Loading...";
|
|
41
48
|
const starsStr = formatStars(skill.stars);
|
|
42
|
-
return (_jsxs("box", { flexDirection: "column", children: [_jsxs("text", { fg: "cyan", children: [_jsx("strong", { children: skill.name }), starsStr ? _jsxs("span", { fg: "yellow", children: [" ", starsStr] }) : null] }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "white", children: description }) }), fm?.category ? (_jsx(KeyValueLine, { label: "Category", value: _jsx("span", { fg: "cyan", children: fm.category }) })) : null, fm?.author ? (_jsx(KeyValueLine, { label: "Author", value: _jsx("span", { children: fm.author }) })) : null, fm?.version ? (_jsx(KeyValueLine, { label: "Version", value: _jsx("span", { children: fm.version }) })) : null, fm?.tags && fm.tags.length > 0 ? (_jsx(KeyValueLine, { label: "Tags", value: _jsx("span", { children: fm.tags.join(", ") }) })) : null, _jsxs(DetailSection, { children: [_jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Source " }), _jsx("span", { fg: "#5c9aff", children: skill.source.repo })] }), _jsxs("text", { children: [_jsx("span", { fg: "gray", children: " " }), _jsx("span", { fg: "gray", children: skill.repoPath })] })] }), skill.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}, paths: {
|
|
46
|
-
user: "~/.claude/skills/",
|
|
47
|
-
project: ".claude/skills/",
|
|
48
|
-
} })] })), skill.hasUpdate && (_jsx("box", { marginTop: 1, children: _jsxs("text", { bg: "yellow", fg: "black", children: [" ", "UPDATE AVAILABLE", " "] }) })), _jsx(ActionHints, { hints: skill.installed
|
|
49
|
-
? [
|
|
50
|
-
{ key: "d", label: "Uninstall", tone: "danger" },
|
|
51
|
-
{ key: "u/p", label: "Reinstall in user/project scope" },
|
|
52
|
-
{ key: "o", label: "Open in browser" },
|
|
53
|
-
]
|
|
54
|
-
: [
|
|
55
|
-
{ key: "u", label: "Install in user scope", tone: "primary" },
|
|
56
|
-
{ key: "p", label: "Install in project scope", tone: "primary" },
|
|
57
|
-
{ key: "o", label: "Open in browser" },
|
|
58
|
-
] })] }));
|
|
49
|
+
return (_jsxs("box", { flexDirection: "column", children: [_jsxs("text", { fg: "cyan", children: [_jsx("strong", { children: skill.name }), starsStr ? _jsxs("span", { fg: "yellow", children: [" ", starsStr] }) : null] }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "white", children: description }) }), fm?.category ? (_jsx(KeyValueLine, { label: "Category", value: _jsx("span", { fg: "cyan", children: fm.category }) })) : null, fm?.author ? (_jsx(KeyValueLine, { label: "Author", value: _jsx("span", { children: fm.author }) })) : null, fm?.version ? (_jsx(KeyValueLine, { label: "Version", value: _jsx("span", { children: fm.version }) })) : null, fm?.tags && fm.tags.length > 0 ? (_jsx(KeyValueLine, { label: "Tags", value: _jsx("span", { children: fm.tags.join(", ") }) })) : null, _jsxs(DetailSection, { children: [_jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Source " }), _jsx("span", { fg: "#5c9aff", children: skill.source.repo })] }), _jsxs("text", { children: [_jsx("span", { fg: "gray", children: " " }), _jsx("span", { fg: "gray", children: skill.repoPath })] })] }), _jsxs(DetailSection, { children: [_jsx("text", { children: "─".repeat(24) }), _jsx("text", { children: _jsx("strong", { children: "Scopes:" }) }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { children: [_jsx("span", { bg: "cyan", fg: "black", children: " u " }), _jsx("span", { fg: skill.installedScope === "user" ? "cyan" : "gray", children: skill.installedScope === "user" ? " ● " : " ○ " }), _jsx("span", { fg: "cyan", children: "User" }), _jsx("span", { fg: "gray", children: " ~/.claude/skills/" })] }), _jsxs("text", { children: [_jsx("span", { bg: "green", fg: "black", children: " p " }), _jsx("span", { fg: skill.installedScope === "project" ? "green" : "gray", children: skill.installedScope === "project" ? " ● " : " ○ " }), _jsx("span", { fg: "green", children: "Project" }), _jsx("span", { fg: "gray", children: " .claude/skills/" })] })] })] }), skill.hasUpdate && (_jsx("box", { marginTop: 1, children: _jsx("text", { bg: "yellow", fg: "black", children: " UPDATE AVAILABLE " }) })), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: skill.installed
|
|
50
|
+
? "Press u/p to toggle scope"
|
|
51
|
+
: "Press u/p to install" }) }), _jsx("box", { children: _jsxs("text", { children: [_jsx("span", { bg: "#555555", fg: "white", children: " o " }), _jsx("span", { fg: "gray", children: " Open in browser" })] }) })] }));
|
|
59
52
|
},
|
|
60
53
|
};
|
|
61
54
|
// ─── Registry ─────────────────────────────────────────────────────────────────
|
|
@@ -62,19 +62,28 @@ const categoryRenderer: ItemRenderer<SkillCategoryItem> = {
|
|
|
62
62
|
|
|
63
63
|
// ─── Skill renderer ───────────────────────────────────────────────────────────
|
|
64
64
|
|
|
65
|
+
const MAX_SKILL_NAME_LEN = 35;
|
|
66
|
+
|
|
67
|
+
function truncateName(name: string): string {
|
|
68
|
+
return name.length > MAX_SKILL_NAME_LEN
|
|
69
|
+
? name.slice(0, MAX_SKILL_NAME_LEN - 1) + "\u2026"
|
|
70
|
+
: name;
|
|
71
|
+
}
|
|
72
|
+
|
|
65
73
|
const skillRenderer: ItemRenderer<SkillSkillItem> = {
|
|
66
74
|
renderRow: ({ item, isSelected }) => {
|
|
67
75
|
const { skill } = item;
|
|
68
76
|
const hasUser = skill.installedScope === "user";
|
|
69
77
|
const hasProject = skill.installedScope === "project";
|
|
70
78
|
const starsStr = formatStars(skill.stars);
|
|
79
|
+
const displayName = truncateName(skill.name);
|
|
71
80
|
|
|
72
81
|
return (
|
|
73
82
|
<SelectableRow selected={isSelected} indent={1}>
|
|
74
83
|
<ScopeSquares user={hasUser} project={hasProject} selected={isSelected} />
|
|
75
84
|
<span> </span>
|
|
76
85
|
<span fg={isSelected ? "white" : skill.installed ? "white" : "gray"}>
|
|
77
|
-
{
|
|
86
|
+
{displayName}
|
|
78
87
|
</span>
|
|
79
88
|
{skill.hasUpdate ? <MetaText text=" ⬆" tone="warning" /> : null}
|
|
80
89
|
{starsStr ? <MetaText text={` ${starsStr}`} tone="warning" /> : null}
|
|
@@ -129,46 +138,48 @@ const skillRenderer: ItemRenderer<SkillSkillItem> = {
|
|
|
129
138
|
</text>
|
|
130
139
|
</DetailSection>
|
|
131
140
|
|
|
132
|
-
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
141
|
+
<DetailSection>
|
|
142
|
+
<text>{"─".repeat(24)}</text>
|
|
143
|
+
<text><strong>Scopes:</strong></text>
|
|
144
|
+
<box marginTop={1} flexDirection="column">
|
|
145
|
+
<text>
|
|
146
|
+
<span bg="cyan" fg="black"> u </span>
|
|
147
|
+
<span fg={skill.installedScope === "user" ? "cyan" : "gray"}>
|
|
148
|
+
{skill.installedScope === "user" ? " ● " : " ○ "}
|
|
149
|
+
</span>
|
|
150
|
+
<span fg="cyan">User</span>
|
|
151
|
+
<span fg="gray"> ~/.claude/skills/</span>
|
|
152
|
+
</text>
|
|
153
|
+
<text>
|
|
154
|
+
<span bg="green" fg="black"> p </span>
|
|
155
|
+
<span fg={skill.installedScope === "project" ? "green" : "gray"}>
|
|
156
|
+
{skill.installedScope === "project" ? " ● " : " ○ "}
|
|
157
|
+
</span>
|
|
158
|
+
<span fg="green">Project</span>
|
|
159
|
+
<span fg="gray"> .claude/skills/</span>
|
|
160
|
+
</text>
|
|
161
|
+
</box>
|
|
162
|
+
</DetailSection>
|
|
147
163
|
|
|
148
164
|
{skill.hasUpdate && (
|
|
149
165
|
<box marginTop={1}>
|
|
150
|
-
<text bg="yellow" fg="black">
|
|
151
|
-
{" "}
|
|
152
|
-
UPDATE AVAILABLE{" "}
|
|
153
|
-
</text>
|
|
166
|
+
<text bg="yellow" fg="black"> UPDATE AVAILABLE </text>
|
|
154
167
|
</box>
|
|
155
168
|
)}
|
|
156
169
|
|
|
157
|
-
<
|
|
158
|
-
|
|
159
|
-
skill.installed
|
|
160
|
-
?
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
/>
|
|
170
|
+
<box marginTop={1}>
|
|
171
|
+
<text fg="gray">
|
|
172
|
+
{skill.installed
|
|
173
|
+
? "Press u/p to toggle scope"
|
|
174
|
+
: "Press u/p to install"}
|
|
175
|
+
</text>
|
|
176
|
+
</box>
|
|
177
|
+
<box>
|
|
178
|
+
<text>
|
|
179
|
+
<span bg="#555555" fg="white"> o </span>
|
|
180
|
+
<span fg="gray"> Open in browser</span>
|
|
181
|
+
</text>
|
|
182
|
+
</box>
|
|
172
183
|
</box>
|
|
173
184
|
);
|
|
174
185
|
},
|
|
@@ -138,7 +138,7 @@ export function SkillsScreen() {
|
|
|
138
138
|
installedScope: isProj ? "project" : isUser ? "user" : null,
|
|
139
139
|
hasUpdate: false,
|
|
140
140
|
isRecommended: true,
|
|
141
|
-
stars:
|
|
141
|
+
stars: r.stars,
|
|
142
142
|
};
|
|
143
143
|
});
|
|
144
144
|
}, [installedFromDisk]);
|
|
@@ -148,7 +148,10 @@ export function SkillsScreen() {
|
|
|
148
148
|
const fetched = skillsState.skills.data.filter((s) => s.isRecommended);
|
|
149
149
|
return staticRecommended.map((staticSkill) => {
|
|
150
150
|
const match = fetched.find((f) => f.source.repo === staticSkill.source.repo && f.name === staticSkill.name);
|
|
151
|
-
|
|
151
|
+
if (!match)
|
|
152
|
+
return staticSkill;
|
|
153
|
+
// Merge: prefer fetched data but keep static stars as fallback
|
|
154
|
+
return { ...staticSkill, ...match, stars: match.stars || staticSkill.stars };
|
|
152
155
|
});
|
|
153
156
|
}, [staticRecommended, skillsState.skills]);
|
|
154
157
|
const installedSkills = useMemo(() => {
|
|
@@ -323,9 +326,6 @@ export function SkillsScreen() {
|
|
|
323
326
|
else
|
|
324
327
|
handleInstall("project");
|
|
325
328
|
}
|
|
326
|
-
else if (event.name === "d" && selectedSkill?.installed) {
|
|
327
|
-
handleUninstall();
|
|
328
|
-
}
|
|
329
329
|
else if (event.name === "r") {
|
|
330
330
|
fetchData();
|
|
331
331
|
}
|
|
@@ -161,7 +161,7 @@ export function SkillsScreen() {
|
|
|
161
161
|
installedScope: isProj ? "project" : isUser ? "user" : null,
|
|
162
162
|
hasUpdate: false,
|
|
163
163
|
isRecommended: true,
|
|
164
|
-
stars:
|
|
164
|
+
stars: r.stars,
|
|
165
165
|
};
|
|
166
166
|
});
|
|
167
167
|
}, [installedFromDisk]);
|
|
@@ -173,7 +173,9 @@ export function SkillsScreen() {
|
|
|
173
173
|
const match = fetched.find(
|
|
174
174
|
(f) => f.source.repo === staticSkill.source.repo && f.name === staticSkill.name,
|
|
175
175
|
);
|
|
176
|
-
|
|
176
|
+
if (!match) return staticSkill;
|
|
177
|
+
// Merge: prefer fetched data but keep static stars as fallback
|
|
178
|
+
return { ...staticSkill, ...match, stars: match.stars || staticSkill.stars };
|
|
177
179
|
});
|
|
178
180
|
}, [staticRecommended, skillsState.skills]);
|
|
179
181
|
|
|
@@ -365,8 +367,6 @@ export function SkillsScreen() {
|
|
|
365
367
|
} else if (event.name === "p" && selectedSkill) {
|
|
366
368
|
if (selectedSkill.installed && selectedSkill.installedScope === "project") handleUninstall();
|
|
367
369
|
else handleInstall("project");
|
|
368
|
-
} else if (event.name === "d" && selectedSkill?.installed) {
|
|
369
|
-
handleUninstall();
|
|
370
370
|
} else if (event.name === "r") {
|
|
371
371
|
fetchData();
|
|
372
372
|
} else if (event.name === "o" && selectedSkill) {
|