claudeup 3.9.0 → 3.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/package.json +1 -1
- package/src/data/skill-repos.js +15 -15
- package/src/data/skill-repos.ts +15 -15
- package/src/services/skills-manager.js +124 -56
- package/src/services/skills-manager.ts +131 -58
- package/src/types/index.ts +4 -0
- package/src/ui/App.js +9 -9
- package/src/ui/App.tsx +9 -9
- package/src/ui/components/EmptyFilterState.js +4 -0
- package/src/ui/components/EmptyFilterState.tsx +27 -0
- package/src/ui/components/TabBar.js +4 -4
- package/src/ui/components/TabBar.tsx +4 -4
- package/src/ui/screens/PluginsScreen.js +32 -47
- package/src/ui/screens/PluginsScreen.tsx +39 -59
- package/src/ui/screens/SkillsScreen.js +248 -143
- package/src/ui/screens/SkillsScreen.tsx +316 -163
package/package.json
CHANGED
package/src/data/skill-repos.js
CHANGED
|
@@ -2,84 +2,84 @@ export const RECOMMENDED_SKILLS = [
|
|
|
2
2
|
{
|
|
3
3
|
name: "Find Skills",
|
|
4
4
|
repo: "vercel-labs/skills",
|
|
5
|
-
skillPath: "find-skills",
|
|
5
|
+
skillPath: "skills/find-skills",
|
|
6
6
|
description: "Discover and install new skills from the ecosystem",
|
|
7
7
|
category: "search",
|
|
8
8
|
},
|
|
9
9
|
{
|
|
10
10
|
name: "React Best Practices",
|
|
11
11
|
repo: "vercel-labs/agent-skills",
|
|
12
|
-
skillPath: "
|
|
12
|
+
skillPath: "skills/react-best-practices",
|
|
13
13
|
description: "Modern React patterns and Vercel deployment guidelines",
|
|
14
14
|
category: "frontend",
|
|
15
15
|
},
|
|
16
16
|
{
|
|
17
17
|
name: "Web Design Guidelines",
|
|
18
18
|
repo: "vercel-labs/agent-skills",
|
|
19
|
-
skillPath: "web-design-guidelines",
|
|
19
|
+
skillPath: "skills/web-design-guidelines",
|
|
20
20
|
description: "UI/UX design principles and web standards",
|
|
21
21
|
category: "design",
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
name: "Remotion Best Practices",
|
|
25
25
|
repo: "remotion-dev/skills",
|
|
26
|
-
skillPath: "remotion-best-practices",
|
|
26
|
+
skillPath: "skills/remotion-best-practices",
|
|
27
27
|
description: "Programmatic video creation with Remotion",
|
|
28
28
|
category: "media",
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
name: "UI/UX Pro Max",
|
|
32
|
-
repo: "
|
|
33
|
-
skillPath: "ui-ux-pro-max",
|
|
34
|
-
description: "
|
|
32
|
+
repo: "jh941213/my-claude-code-asset",
|
|
33
|
+
skillPath: "skills_en/ui-ux-pro-max",
|
|
34
|
+
description: "50 styles, 21 palettes, 50 font pairings, 9 stacks. Covers React, Next.js, Vue, Svelte, SwiftUI, Flutter, Tailwind, shadcn/ui",
|
|
35
35
|
category: "design",
|
|
36
36
|
},
|
|
37
37
|
{
|
|
38
38
|
name: "ElevenLabs TTS",
|
|
39
39
|
repo: "inferen-sh/skills",
|
|
40
|
-
skillPath: "elevenlabs-tts",
|
|
40
|
+
skillPath: "skills/elevenlabs-tts",
|
|
41
41
|
description: "Text-to-speech with ElevenLabs API integration",
|
|
42
42
|
category: "media",
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
45
|
name: "Audit Website",
|
|
46
46
|
repo: "squirrelscan/skills",
|
|
47
|
-
skillPath: "audit-website",
|
|
47
|
+
skillPath: "skills/audit-website",
|
|
48
48
|
description: "Security and quality auditing for web applications",
|
|
49
49
|
category: "security",
|
|
50
50
|
},
|
|
51
51
|
{
|
|
52
52
|
name: "Systematic Debugging",
|
|
53
53
|
repo: "obra/superpowers",
|
|
54
|
-
skillPath: "systematic-debugging",
|
|
54
|
+
skillPath: "skills/systematic-debugging",
|
|
55
55
|
description: "Structured debugging methodology with root cause analysis",
|
|
56
56
|
category: "debugging",
|
|
57
57
|
},
|
|
58
58
|
{
|
|
59
59
|
name: "shadcn/ui",
|
|
60
|
-
repo: "shadcn/ui",
|
|
61
|
-
skillPath: "shadcn",
|
|
60
|
+
repo: "shadcn-ui/ui",
|
|
61
|
+
skillPath: "packages/shadcn",
|
|
62
62
|
description: "shadcn/ui component library patterns and usage",
|
|
63
63
|
category: "frontend",
|
|
64
64
|
},
|
|
65
65
|
{
|
|
66
66
|
name: "Neon Postgres",
|
|
67
67
|
repo: "neondatabase/agent-skills",
|
|
68
|
-
skillPath: "neon-postgres",
|
|
68
|
+
skillPath: "skills/neon-postgres",
|
|
69
69
|
description: "Neon serverless Postgres setup and best practices",
|
|
70
70
|
category: "database",
|
|
71
71
|
},
|
|
72
72
|
{
|
|
73
73
|
name: "Neon Serverless",
|
|
74
74
|
repo: "neondatabase/ai-rules",
|
|
75
|
-
skillPath: "neon-serverless",
|
|
75
|
+
skillPath: "skills/neon-serverless",
|
|
76
76
|
description: "Serverless database patterns with Neon",
|
|
77
77
|
category: "database",
|
|
78
78
|
},
|
|
79
79
|
];
|
|
80
80
|
export const DEFAULT_SKILL_REPOS = [
|
|
81
81
|
{
|
|
82
|
-
label: "
|
|
82
|
+
label: "Vercel Agent Skills",
|
|
83
83
|
repo: "vercel-labs/agent-skills",
|
|
84
84
|
skillsPath: "skills",
|
|
85
85
|
},
|
package/src/data/skill-repos.ts
CHANGED
|
@@ -12,77 +12,77 @@ export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
|
|
|
12
12
|
{
|
|
13
13
|
name: "Find Skills",
|
|
14
14
|
repo: "vercel-labs/skills",
|
|
15
|
-
skillPath: "find-skills",
|
|
15
|
+
skillPath: "skills/find-skills",
|
|
16
16
|
description: "Discover and install new skills from the ecosystem",
|
|
17
17
|
category: "search",
|
|
18
18
|
},
|
|
19
19
|
{
|
|
20
20
|
name: "React Best Practices",
|
|
21
21
|
repo: "vercel-labs/agent-skills",
|
|
22
|
-
skillPath: "
|
|
22
|
+
skillPath: "skills/react-best-practices",
|
|
23
23
|
description: "Modern React patterns and Vercel deployment guidelines",
|
|
24
24
|
category: "frontend",
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
name: "Web Design Guidelines",
|
|
28
28
|
repo: "vercel-labs/agent-skills",
|
|
29
|
-
skillPath: "web-design-guidelines",
|
|
29
|
+
skillPath: "skills/web-design-guidelines",
|
|
30
30
|
description: "UI/UX design principles and web standards",
|
|
31
31
|
category: "design",
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
name: "Remotion Best Practices",
|
|
35
35
|
repo: "remotion-dev/skills",
|
|
36
|
-
skillPath: "remotion-best-practices",
|
|
36
|
+
skillPath: "skills/remotion-best-practices",
|
|
37
37
|
description: "Programmatic video creation with Remotion",
|
|
38
38
|
category: "media",
|
|
39
39
|
},
|
|
40
40
|
{
|
|
41
41
|
name: "UI/UX Pro Max",
|
|
42
|
-
repo: "
|
|
43
|
-
skillPath: "ui-ux-pro-max",
|
|
44
|
-
description: "
|
|
42
|
+
repo: "jh941213/my-claude-code-asset",
|
|
43
|
+
skillPath: "skills_en/ui-ux-pro-max",
|
|
44
|
+
description: "50 styles, 21 palettes, 50 font pairings, 9 stacks. Covers React, Next.js, Vue, Svelte, SwiftUI, Flutter, Tailwind, shadcn/ui",
|
|
45
45
|
category: "design",
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
48
|
name: "ElevenLabs TTS",
|
|
49
49
|
repo: "inferen-sh/skills",
|
|
50
|
-
skillPath: "elevenlabs-tts",
|
|
50
|
+
skillPath: "skills/elevenlabs-tts",
|
|
51
51
|
description: "Text-to-speech with ElevenLabs API integration",
|
|
52
52
|
category: "media",
|
|
53
53
|
},
|
|
54
54
|
{
|
|
55
55
|
name: "Audit Website",
|
|
56
56
|
repo: "squirrelscan/skills",
|
|
57
|
-
skillPath: "audit-website",
|
|
57
|
+
skillPath: "skills/audit-website",
|
|
58
58
|
description: "Security and quality auditing for web applications",
|
|
59
59
|
category: "security",
|
|
60
60
|
},
|
|
61
61
|
{
|
|
62
62
|
name: "Systematic Debugging",
|
|
63
63
|
repo: "obra/superpowers",
|
|
64
|
-
skillPath: "systematic-debugging",
|
|
64
|
+
skillPath: "skills/systematic-debugging",
|
|
65
65
|
description: "Structured debugging methodology with root cause analysis",
|
|
66
66
|
category: "debugging",
|
|
67
67
|
},
|
|
68
68
|
{
|
|
69
69
|
name: "shadcn/ui",
|
|
70
|
-
repo: "shadcn/ui",
|
|
71
|
-
skillPath: "shadcn",
|
|
70
|
+
repo: "shadcn-ui/ui",
|
|
71
|
+
skillPath: "packages/shadcn",
|
|
72
72
|
description: "shadcn/ui component library patterns and usage",
|
|
73
73
|
category: "frontend",
|
|
74
74
|
},
|
|
75
75
|
{
|
|
76
76
|
name: "Neon Postgres",
|
|
77
77
|
repo: "neondatabase/agent-skills",
|
|
78
|
-
skillPath: "neon-postgres",
|
|
78
|
+
skillPath: "skills/neon-postgres",
|
|
79
79
|
description: "Neon serverless Postgres setup and best practices",
|
|
80
80
|
category: "database",
|
|
81
81
|
},
|
|
82
82
|
{
|
|
83
83
|
name: "Neon Serverless",
|
|
84
84
|
repo: "neondatabase/ai-rules",
|
|
85
|
-
skillPath: "neon-serverless",
|
|
85
|
+
skillPath: "skills/neon-serverless",
|
|
86
86
|
description: "Serverless database patterns with Neon",
|
|
87
87
|
category: "database",
|
|
88
88
|
},
|
|
@@ -90,7 +90,7 @@ export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
|
|
|
90
90
|
|
|
91
91
|
export const DEFAULT_SKILL_REPOS: SkillSource[] = [
|
|
92
92
|
{
|
|
93
|
-
label: "
|
|
93
|
+
label: "Vercel Agent Skills",
|
|
94
94
|
repo: "vercel-labs/agent-skills",
|
|
95
95
|
skillsPath: "skills",
|
|
96
96
|
},
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from "fs-extra";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import os from "node:os";
|
|
4
|
+
import { RECOMMENDED_SKILLS } from "../data/skill-repos.js";
|
|
5
|
+
const SKILLS_API_BASE = "https://us-central1-claudish-6da10.cloudfunctions.net/skills";
|
|
4
6
|
const treeCache = new Map();
|
|
5
7
|
// ─── Path helpers ──────────────────────────────────────────────────────────────
|
|
6
8
|
export function getUserSkillsDir() {
|
|
@@ -122,72 +124,138 @@ export async function getInstalledSkillNames(scope, projectPath) {
|
|
|
122
124
|
}
|
|
123
125
|
return installed;
|
|
124
126
|
}
|
|
127
|
+
// ─── Firebase Skills API ──────────────────────────────────────────────────────
|
|
128
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
129
|
+
function mapApiSkillToSkillInfo(raw) {
|
|
130
|
+
const repo = raw.repo || "unknown";
|
|
131
|
+
const skillPath = raw.skillPath || raw.name || "";
|
|
132
|
+
const source = {
|
|
133
|
+
label: repo,
|
|
134
|
+
repo,
|
|
135
|
+
skillsPath: "",
|
|
136
|
+
};
|
|
137
|
+
return {
|
|
138
|
+
id: `${repo}/${skillPath}`,
|
|
139
|
+
name: raw.name || skillPath,
|
|
140
|
+
description: raw.description || "",
|
|
141
|
+
source,
|
|
142
|
+
repoPath: skillPath ? `${skillPath}/SKILL.md` : "SKILL.md",
|
|
143
|
+
gitBlobSha: "",
|
|
144
|
+
frontmatter: null,
|
|
145
|
+
installed: false,
|
|
146
|
+
installedScope: null,
|
|
147
|
+
hasUpdate: false,
|
|
148
|
+
stars: typeof raw.stars === "number" ? raw.stars : undefined,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
export async function fetchPopularSkills(limit = 30) {
|
|
152
|
+
try {
|
|
153
|
+
const res = await fetch(`${SKILLS_API_BASE}/search?q=development&limit=${limit}&sortBy=stars`, { signal: AbortSignal.timeout(10000) });
|
|
154
|
+
if (!res.ok)
|
|
155
|
+
return [];
|
|
156
|
+
const data = (await res.json());
|
|
157
|
+
return (data.skills || []).map(mapApiSkillToSkillInfo);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
125
163
|
// ─── Fetch available skills ───────────────────────────────────────────────────
|
|
126
|
-
export async function fetchAvailableSkills(
|
|
164
|
+
export async function fetchAvailableSkills(_repos, projectPath) {
|
|
127
165
|
const userInstalled = await getInstalledSkillNames("user");
|
|
128
166
|
const projectInstalled = await getInstalledSkillNames("project", projectPath);
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
167
|
+
const markInstalled = (skill) => {
|
|
168
|
+
const isUserInstalled = userInstalled.has(skill.name);
|
|
169
|
+
const isProjInstalled = projectInstalled.has(skill.name);
|
|
170
|
+
const installed = isUserInstalled || isProjInstalled;
|
|
171
|
+
const installedScope = isProjInstalled
|
|
172
|
+
? "project"
|
|
173
|
+
: isUserInstalled
|
|
174
|
+
? "user"
|
|
175
|
+
: null;
|
|
176
|
+
return { ...skill, installed, installedScope };
|
|
177
|
+
};
|
|
178
|
+
// 1. Recommended skills from RECOMMENDED_SKILLS constant (no API call)
|
|
179
|
+
const recommendedSkills = RECOMMENDED_SKILLS.map((rec) => {
|
|
180
|
+
const source = {
|
|
181
|
+
label: rec.repo,
|
|
182
|
+
repo: rec.repo,
|
|
183
|
+
skillsPath: "",
|
|
184
|
+
};
|
|
185
|
+
const skill = {
|
|
186
|
+
id: `${rec.repo}/${rec.skillPath}`,
|
|
187
|
+
name: rec.name,
|
|
188
|
+
description: rec.description,
|
|
189
|
+
source,
|
|
190
|
+
repoPath: `${rec.skillPath}/SKILL.md`,
|
|
191
|
+
gitBlobSha: "",
|
|
192
|
+
frontmatter: null,
|
|
193
|
+
installed: false,
|
|
194
|
+
installedScope: null,
|
|
195
|
+
hasUpdate: false,
|
|
196
|
+
isRecommended: true,
|
|
197
|
+
};
|
|
198
|
+
return markInstalled(skill);
|
|
199
|
+
});
|
|
200
|
+
// 2. Fetch popular skills from Firebase API
|
|
201
|
+
const popular = await fetchPopularSkills(30);
|
|
202
|
+
const popularSkills = popular.map((s) => markInstalled({ ...s, isRecommended: false }));
|
|
203
|
+
// 3. Enrich recommended skills with GitHub repo stars
|
|
204
|
+
// Fetch stars for each unique repo (typically ~7 repos, parallel)
|
|
205
|
+
const uniqueRepos = [...new Set(recommendedSkills.map((s) => s.source.repo))];
|
|
206
|
+
const repoStars = new Map();
|
|
207
|
+
try {
|
|
208
|
+
const starResults = await Promise.allSettled(uniqueRepos.map(async (repo) => {
|
|
209
|
+
const res = await fetch(`https://api.github.com/repos/${repo}`, {
|
|
210
|
+
headers: { Accept: "application/vnd.github+json" },
|
|
211
|
+
signal: AbortSignal.timeout(5000),
|
|
212
|
+
});
|
|
213
|
+
if (!res.ok)
|
|
214
|
+
return;
|
|
215
|
+
const data = (await res.json());
|
|
216
|
+
if (data.stargazers_count)
|
|
217
|
+
repoStars.set(repo, data.stargazers_count);
|
|
218
|
+
}));
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
// Non-fatal — stars are cosmetic
|
|
222
|
+
}
|
|
223
|
+
for (const rec of recommendedSkills) {
|
|
224
|
+
rec.stars = repoStars.get(rec.source.repo) || undefined;
|
|
225
|
+
}
|
|
226
|
+
// 4. Combine: recommended first, then popular (dedup by name)
|
|
227
|
+
const seen = new Set(recommendedSkills.map((s) => s.name));
|
|
228
|
+
const deduped = popularSkills.filter((s) => !seen.has(s.name));
|
|
229
|
+
return [...recommendedSkills, ...deduped];
|
|
230
|
+
}
|
|
231
|
+
// ─── Install / Uninstall ──────────────────────────────────────────────────────
|
|
232
|
+
export async function installSkill(skill, scope, projectPath) {
|
|
233
|
+
// Try multiple URL patterns — repos structure SKILL.md differently
|
|
234
|
+
const repo = skill.source.repo;
|
|
235
|
+
const repoPath = skill.repoPath.replace(/\/SKILL\.md$/, "");
|
|
236
|
+
const candidates = [
|
|
237
|
+
`https://raw.githubusercontent.com/${repo}/HEAD/${repoPath}/SKILL.md`,
|
|
238
|
+
`https://raw.githubusercontent.com/${repo}/main/${repoPath}/SKILL.md`,
|
|
239
|
+
`https://raw.githubusercontent.com/${repo}/master/${repoPath}/SKILL.md`,
|
|
240
|
+
`https://raw.githubusercontent.com/${repo}/HEAD/SKILL.md`,
|
|
241
|
+
`https://raw.githubusercontent.com/${repo}/main/SKILL.md`,
|
|
242
|
+
];
|
|
243
|
+
let content = null;
|
|
244
|
+
for (const url of candidates) {
|
|
132
245
|
try {
|
|
133
|
-
|
|
246
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(8000) });
|
|
247
|
+
if (response.ok) {
|
|
248
|
+
content = await response.text();
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
134
251
|
}
|
|
135
252
|
catch {
|
|
136
|
-
// Skip this repo on error
|
|
137
253
|
continue;
|
|
138
254
|
}
|
|
139
|
-
// Filter for SKILL.md files under skillsPath
|
|
140
|
-
const prefix = source.skillsPath ? `${source.skillsPath}/` : "";
|
|
141
|
-
for (const item of tree.tree) {
|
|
142
|
-
if (item.type !== "blob")
|
|
143
|
-
continue;
|
|
144
|
-
if (!item.path.endsWith("/SKILL.md"))
|
|
145
|
-
continue;
|
|
146
|
-
if (prefix && !item.path.startsWith(prefix))
|
|
147
|
-
continue;
|
|
148
|
-
// Extract skill name: second-to-last segment of path
|
|
149
|
-
const parts = item.path.split("/");
|
|
150
|
-
if (parts.length < 2)
|
|
151
|
-
continue;
|
|
152
|
-
const skillName = parts[parts.length - 2];
|
|
153
|
-
// Validate name (prevent traversal)
|
|
154
|
-
if (!/^[a-z0-9][a-z0-9-_]*$/i.test(skillName))
|
|
155
|
-
continue;
|
|
156
|
-
const isUserInstalled = userInstalled.has(skillName);
|
|
157
|
-
const isProjInstalled = projectInstalled.has(skillName);
|
|
158
|
-
const installed = isUserInstalled || isProjInstalled;
|
|
159
|
-
const installedScope = isProjInstalled
|
|
160
|
-
? "project"
|
|
161
|
-
: isUserInstalled
|
|
162
|
-
? "user"
|
|
163
|
-
: null;
|
|
164
|
-
skills.push({
|
|
165
|
-
id: `${source.repo}/${item.path}`,
|
|
166
|
-
name: skillName,
|
|
167
|
-
source,
|
|
168
|
-
repoPath: item.path,
|
|
169
|
-
gitBlobSha: item.sha,
|
|
170
|
-
frontmatter: null,
|
|
171
|
-
installed,
|
|
172
|
-
installedScope,
|
|
173
|
-
hasUpdate: false,
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
255
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
return skills;
|
|
180
|
-
}
|
|
181
|
-
// ─── Install / Uninstall ──────────────────────────────────────────────────────
|
|
182
|
-
export async function installSkill(skill, scope, projectPath) {
|
|
183
|
-
const url = `https://raw.githubusercontent.com/${skill.source.repo}/HEAD/${skill.repoPath}`;
|
|
184
|
-
const response = await fetch(url, {
|
|
185
|
-
signal: AbortSignal.timeout(15000),
|
|
186
|
-
});
|
|
187
|
-
if (!response.ok) {
|
|
188
|
-
throw new Error(`Failed to fetch skill: ${response.status} ${response.statusText}`);
|
|
256
|
+
if (!content) {
|
|
257
|
+
throw new Error(`Failed to fetch skill: SKILL.md not found in ${repo}/${repoPath}`);
|
|
189
258
|
}
|
|
190
|
-
const content = await response.text();
|
|
191
259
|
const installDir = scope === "user"
|
|
192
260
|
? path.join(getUserSkillsDir(), skill.name)
|
|
193
261
|
: path.join(getProjectSkillsDir(projectPath), skill.name);
|
|
@@ -7,6 +7,10 @@ import type {
|
|
|
7
7
|
SkillFrontmatter,
|
|
8
8
|
GitTreeResponse,
|
|
9
9
|
} from "../types/index.js";
|
|
10
|
+
import { RECOMMENDED_SKILLS } from "../data/skill-repos.js";
|
|
11
|
+
|
|
12
|
+
const SKILLS_API_BASE =
|
|
13
|
+
"https://us-central1-claudish-6da10.cloudfunctions.net/skills";
|
|
10
14
|
|
|
11
15
|
// ─── In-process cache ─────────────────────────────────────────────────────────
|
|
12
16
|
|
|
@@ -175,69 +179,122 @@ export async function getInstalledSkillNames(
|
|
|
175
179
|
return installed;
|
|
176
180
|
}
|
|
177
181
|
|
|
182
|
+
// ─── Firebase Skills API ──────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
185
|
+
function mapApiSkillToSkillInfo(raw: any): SkillInfo {
|
|
186
|
+
const repo = (raw.repo as string) || "unknown";
|
|
187
|
+
const skillPath = (raw.skillPath as string) || (raw.name as string) || "";
|
|
188
|
+
const source: SkillSource = {
|
|
189
|
+
label: repo,
|
|
190
|
+
repo,
|
|
191
|
+
skillsPath: "",
|
|
192
|
+
};
|
|
193
|
+
return {
|
|
194
|
+
id: `${repo}/${skillPath}`,
|
|
195
|
+
name: (raw.name as string) || skillPath,
|
|
196
|
+
description: (raw.description as string) || "",
|
|
197
|
+
source,
|
|
198
|
+
repoPath: skillPath ? `${skillPath}/SKILL.md` : "SKILL.md",
|
|
199
|
+
gitBlobSha: "",
|
|
200
|
+
frontmatter: null,
|
|
201
|
+
installed: false,
|
|
202
|
+
installedScope: null,
|
|
203
|
+
hasUpdate: false,
|
|
204
|
+
stars: typeof raw.stars === "number" ? raw.stars : undefined,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export async function fetchPopularSkills(limit = 30): Promise<SkillInfo[]> {
|
|
209
|
+
try {
|
|
210
|
+
const res = await fetch(
|
|
211
|
+
`${SKILLS_API_BASE}/search?q=development&limit=${limit}&sortBy=stars`,
|
|
212
|
+
{ signal: AbortSignal.timeout(10000) },
|
|
213
|
+
);
|
|
214
|
+
if (!res.ok) return [];
|
|
215
|
+
const data = (await res.json()) as { skills?: unknown[] };
|
|
216
|
+
return (data.skills || []).map(mapApiSkillToSkillInfo);
|
|
217
|
+
} catch {
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
178
222
|
// ─── Fetch available skills ───────────────────────────────────────────────────
|
|
179
223
|
|
|
180
224
|
export async function fetchAvailableSkills(
|
|
181
|
-
|
|
225
|
+
_repos: SkillSource[],
|
|
182
226
|
projectPath?: string,
|
|
183
227
|
): Promise<SkillInfo[]> {
|
|
184
228
|
const userInstalled = await getInstalledSkillNames("user");
|
|
185
229
|
const projectInstalled = await getInstalledSkillNames("project", projectPath);
|
|
186
230
|
|
|
187
|
-
const
|
|
231
|
+
const markInstalled = (skill: SkillInfo): SkillInfo => {
|
|
232
|
+
const isUserInstalled = userInstalled.has(skill.name);
|
|
233
|
+
const isProjInstalled = projectInstalled.has(skill.name);
|
|
234
|
+
const installed = isUserInstalled || isProjInstalled;
|
|
235
|
+
const installedScope: "user" | "project" | null = isProjInstalled
|
|
236
|
+
? "project"
|
|
237
|
+
: isUserInstalled
|
|
238
|
+
? "user"
|
|
239
|
+
: null;
|
|
240
|
+
return { ...skill, installed, installedScope };
|
|
241
|
+
};
|
|
188
242
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
243
|
+
// 1. Recommended skills from RECOMMENDED_SKILLS constant (no API call)
|
|
244
|
+
const recommendedSkills: SkillInfo[] = RECOMMENDED_SKILLS.map((rec) => {
|
|
245
|
+
const source: SkillSource = {
|
|
246
|
+
label: rec.repo,
|
|
247
|
+
repo: rec.repo,
|
|
248
|
+
skillsPath: "",
|
|
249
|
+
};
|
|
250
|
+
const skill: SkillInfo = {
|
|
251
|
+
id: `${rec.repo}/${rec.skillPath}`,
|
|
252
|
+
name: rec.name,
|
|
253
|
+
description: rec.description,
|
|
254
|
+
source,
|
|
255
|
+
repoPath: `${rec.skillPath}/SKILL.md`,
|
|
256
|
+
gitBlobSha: "",
|
|
257
|
+
frontmatter: null,
|
|
258
|
+
installed: false,
|
|
259
|
+
installedScope: null,
|
|
260
|
+
hasUpdate: false,
|
|
261
|
+
isRecommended: true,
|
|
262
|
+
};
|
|
263
|
+
return markInstalled(skill);
|
|
264
|
+
});
|
|
197
265
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
skills.push({
|
|
224
|
-
id: `${source.repo}/${item.path}`,
|
|
225
|
-
name: skillName,
|
|
226
|
-
source,
|
|
227
|
-
repoPath: item.path,
|
|
228
|
-
gitBlobSha: item.sha,
|
|
229
|
-
frontmatter: null,
|
|
230
|
-
installed,
|
|
231
|
-
installedScope,
|
|
232
|
-
hasUpdate: false,
|
|
233
|
-
});
|
|
234
|
-
}
|
|
266
|
+
// 2. Fetch popular skills from Firebase API
|
|
267
|
+
const popular = await fetchPopularSkills(30);
|
|
268
|
+
const popularSkills = popular.map((s) => markInstalled({ ...s, isRecommended: false }));
|
|
269
|
+
|
|
270
|
+
// 3. Enrich recommended skills with GitHub repo stars
|
|
271
|
+
// Fetch stars for each unique repo (typically ~7 repos, parallel)
|
|
272
|
+
const uniqueRepos = [...new Set(recommendedSkills.map((s) => s.source.repo))];
|
|
273
|
+
const repoStars = new Map<string, number>();
|
|
274
|
+
try {
|
|
275
|
+
const starResults = await Promise.allSettled(
|
|
276
|
+
uniqueRepos.map(async (repo) => {
|
|
277
|
+
const res = await fetch(`https://api.github.com/repos/${repo}`, {
|
|
278
|
+
headers: { Accept: "application/vnd.github+json" },
|
|
279
|
+
signal: AbortSignal.timeout(5000),
|
|
280
|
+
});
|
|
281
|
+
if (!res.ok) return;
|
|
282
|
+
const data = (await res.json()) as { stargazers_count?: number };
|
|
283
|
+
if (data.stargazers_count) repoStars.set(repo, data.stargazers_count);
|
|
284
|
+
}),
|
|
285
|
+
);
|
|
286
|
+
} catch {
|
|
287
|
+
// Non-fatal — stars are cosmetic
|
|
288
|
+
}
|
|
289
|
+
for (const rec of recommendedSkills) {
|
|
290
|
+
rec.stars = repoStars.get(rec.source.repo) || undefined;
|
|
235
291
|
}
|
|
236
292
|
|
|
237
|
-
//
|
|
238
|
-
|
|
293
|
+
// 4. Combine: recommended first, then popular (dedup by name)
|
|
294
|
+
const seen = new Set<string>(recommendedSkills.map((s) => s.name));
|
|
295
|
+
const deduped = popularSkills.filter((s) => !seen.has(s.name));
|
|
239
296
|
|
|
240
|
-
return
|
|
297
|
+
return [...recommendedSkills, ...deduped];
|
|
241
298
|
}
|
|
242
299
|
|
|
243
300
|
// ─── Install / Uninstall ──────────────────────────────────────────────────────
|
|
@@ -247,20 +304,36 @@ export async function installSkill(
|
|
|
247
304
|
scope: "user" | "project",
|
|
248
305
|
projectPath?: string,
|
|
249
306
|
): Promise<void> {
|
|
250
|
-
|
|
307
|
+
// Try multiple URL patterns — repos structure SKILL.md differently
|
|
308
|
+
const repo = skill.source.repo;
|
|
309
|
+
const repoPath = skill.repoPath.replace(/\/SKILL\.md$/, "");
|
|
310
|
+
const candidates = [
|
|
311
|
+
`https://raw.githubusercontent.com/${repo}/HEAD/${repoPath}/SKILL.md`,
|
|
312
|
+
`https://raw.githubusercontent.com/${repo}/main/${repoPath}/SKILL.md`,
|
|
313
|
+
`https://raw.githubusercontent.com/${repo}/master/${repoPath}/SKILL.md`,
|
|
314
|
+
`https://raw.githubusercontent.com/${repo}/HEAD/SKILL.md`,
|
|
315
|
+
`https://raw.githubusercontent.com/${repo}/main/SKILL.md`,
|
|
316
|
+
];
|
|
251
317
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
318
|
+
let content: string | null = null;
|
|
319
|
+
for (const url of candidates) {
|
|
320
|
+
try {
|
|
321
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(8000) });
|
|
322
|
+
if (response.ok) {
|
|
323
|
+
content = await response.text();
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
} catch {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
255
330
|
|
|
256
|
-
if (!
|
|
331
|
+
if (!content) {
|
|
257
332
|
throw new Error(
|
|
258
|
-
`Failed to fetch skill:
|
|
333
|
+
`Failed to fetch skill: SKILL.md not found in ${repo}/${repoPath}`,
|
|
259
334
|
);
|
|
260
335
|
}
|
|
261
336
|
|
|
262
|
-
const content = await response.text();
|
|
263
|
-
|
|
264
337
|
const installDir =
|
|
265
338
|
scope === "user"
|
|
266
339
|
? path.join(getUserSkillsDir(), skill.name)
|
package/src/types/index.ts
CHANGED
|
@@ -164,6 +164,10 @@ export interface SkillInfo {
|
|
|
164
164
|
hasUpdate: boolean;
|
|
165
165
|
/** Whether this is a recommended skill */
|
|
166
166
|
isRecommended?: boolean;
|
|
167
|
+
/** GitHub star count from the skills API */
|
|
168
|
+
stars?: number;
|
|
169
|
+
/** Short description (from API, before frontmatter is loaded) */
|
|
170
|
+
description?: string;
|
|
167
171
|
}
|
|
168
172
|
|
|
169
173
|
// ─── GitHub Tree API types ────────────────────────────────────────────────────
|