claudeup 3.9.0 → 3.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeup",
3
- "version": "3.9.0",
3
+ "version": "3.10.0",
4
4
  "description": "TUI tool for managing Claude Code plugins, MCPs, and configuration",
5
5
  "type": "module",
6
6
  "main": "src/main.tsx",
@@ -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: "vercel-react-best-practices",
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: "nextlevelbuilder/ui-ux-pro-max-skill",
33
- skillPath: "ui-ux-pro-max",
34
- description: "Advanced UI/UX design and implementation patterns",
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: "vercel-labs/agent-skills",
82
+ label: "Vercel Agent Skills",
83
83
  repo: "vercel-labs/agent-skills",
84
84
  skillsPath: "skills",
85
85
  },
@@ -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: "vercel-react-best-practices",
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: "nextlevelbuilder/ui-ux-pro-max-skill",
43
- skillPath: "ui-ux-pro-max",
44
- description: "Advanced UI/UX design and implementation patterns",
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: "vercel-labs/agent-skills",
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(repos, projectPath) {
164
+ export async function fetchAvailableSkills(_repos, projectPath) {
127
165
  const userInstalled = await getInstalledSkillNames("user");
128
166
  const projectInstalled = await getInstalledSkillNames("project", projectPath);
129
- const skills = [];
130
- for (const source of repos) {
131
- let tree;
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
- tree = await fetchGitTree(source.repo);
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
- // Sort by name within each repo
178
- skills.sort((a, b) => a.name.localeCompare(b.name));
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
- repos: SkillSource[],
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 skills: SkillInfo[] = [];
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
- for (const source of repos) {
190
- let tree: GitTreeResponse;
191
- try {
192
- tree = await fetchGitTree(source.repo);
193
- } catch {
194
- // Skip this repo on error
195
- continue;
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
- // Filter for SKILL.md files under skillsPath
199
- const prefix = source.skillsPath ? `${source.skillsPath}/` : "";
200
-
201
- for (const item of tree.tree) {
202
- if (item.type !== "blob") continue;
203
- if (!item.path.endsWith("/SKILL.md")) continue;
204
- if (prefix && !item.path.startsWith(prefix)) continue;
205
-
206
- // Extract skill name: second-to-last segment of path
207
- const parts = item.path.split("/");
208
- if (parts.length < 2) continue;
209
- const skillName = parts[parts.length - 2];
210
-
211
- // Validate name (prevent traversal)
212
- if (!/^[a-z0-9][a-z0-9-_]*$/i.test(skillName)) continue;
213
-
214
- const isUserInstalled = userInstalled.has(skillName);
215
- const isProjInstalled = projectInstalled.has(skillName);
216
- const installed = isUserInstalled || isProjInstalled;
217
- const installedScope: "user" | "project" | null = isProjInstalled
218
- ? "project"
219
- : isUserInstalled
220
- ? "user"
221
- : null;
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
- // Sort by name within each repo
238
- skills.sort((a, b) => a.name.localeCompare(b.name));
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 skills;
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
- const url = `https://raw.githubusercontent.com/${skill.source.repo}/HEAD/${skill.repoPath}`;
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
- const response = await fetch(url, {
253
- signal: AbortSignal.timeout(15000),
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 (!response.ok) {
331
+ if (!content) {
257
332
  throw new Error(
258
- `Failed to fetch skill: ${response.status} ${response.statusText}`,
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)
@@ -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 ────────────────────────────────────────────────────