claudeup 3.8.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.8.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",
@@ -0,0 +1,86 @@
1
+ export const RECOMMENDED_SKILLS = [
2
+ {
3
+ name: "Find Skills",
4
+ repo: "vercel-labs/skills",
5
+ skillPath: "skills/find-skills",
6
+ description: "Discover and install new skills from the ecosystem",
7
+ category: "search",
8
+ },
9
+ {
10
+ name: "React Best Practices",
11
+ repo: "vercel-labs/agent-skills",
12
+ skillPath: "skills/react-best-practices",
13
+ description: "Modern React patterns and Vercel deployment guidelines",
14
+ category: "frontend",
15
+ },
16
+ {
17
+ name: "Web Design Guidelines",
18
+ repo: "vercel-labs/agent-skills",
19
+ skillPath: "skills/web-design-guidelines",
20
+ description: "UI/UX design principles and web standards",
21
+ category: "design",
22
+ },
23
+ {
24
+ name: "Remotion Best Practices",
25
+ repo: "remotion-dev/skills",
26
+ skillPath: "skills/remotion-best-practices",
27
+ description: "Programmatic video creation with Remotion",
28
+ category: "media",
29
+ },
30
+ {
31
+ name: "UI/UX Pro Max",
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
+ category: "design",
36
+ },
37
+ {
38
+ name: "ElevenLabs TTS",
39
+ repo: "inferen-sh/skills",
40
+ skillPath: "skills/elevenlabs-tts",
41
+ description: "Text-to-speech with ElevenLabs API integration",
42
+ category: "media",
43
+ },
44
+ {
45
+ name: "Audit Website",
46
+ repo: "squirrelscan/skills",
47
+ skillPath: "skills/audit-website",
48
+ description: "Security and quality auditing for web applications",
49
+ category: "security",
50
+ },
51
+ {
52
+ name: "Systematic Debugging",
53
+ repo: "obra/superpowers",
54
+ skillPath: "skills/systematic-debugging",
55
+ description: "Structured debugging methodology with root cause analysis",
56
+ category: "debugging",
57
+ },
58
+ {
59
+ name: "shadcn/ui",
60
+ repo: "shadcn-ui/ui",
61
+ skillPath: "packages/shadcn",
62
+ description: "shadcn/ui component library patterns and usage",
63
+ category: "frontend",
64
+ },
65
+ {
66
+ name: "Neon Postgres",
67
+ repo: "neondatabase/agent-skills",
68
+ skillPath: "skills/neon-postgres",
69
+ description: "Neon serverless Postgres setup and best practices",
70
+ category: "database",
71
+ },
72
+ {
73
+ name: "Neon Serverless",
74
+ repo: "neondatabase/ai-rules",
75
+ skillPath: "skills/neon-serverless",
76
+ description: "Serverless database patterns with Neon",
77
+ category: "database",
78
+ },
79
+ ];
80
+ export const DEFAULT_SKILL_REPOS = [
81
+ {
82
+ label: "Vercel Agent Skills",
83
+ repo: "vercel-labs/agent-skills",
84
+ skillsPath: "skills",
85
+ },
86
+ ];
@@ -0,0 +1,97 @@
1
+ import type { SkillSource } from "../types/index.js";
2
+
3
+ export interface RecommendedSkill {
4
+ name: string;
5
+ repo: string;
6
+ skillPath: string;
7
+ description: string;
8
+ category: string;
9
+ }
10
+
11
+ export const RECOMMENDED_SKILLS: RecommendedSkill[] = [
12
+ {
13
+ name: "Find Skills",
14
+ repo: "vercel-labs/skills",
15
+ skillPath: "skills/find-skills",
16
+ description: "Discover and install new skills from the ecosystem",
17
+ category: "search",
18
+ },
19
+ {
20
+ name: "React Best Practices",
21
+ repo: "vercel-labs/agent-skills",
22
+ skillPath: "skills/react-best-practices",
23
+ description: "Modern React patterns and Vercel deployment guidelines",
24
+ category: "frontend",
25
+ },
26
+ {
27
+ name: "Web Design Guidelines",
28
+ repo: "vercel-labs/agent-skills",
29
+ skillPath: "skills/web-design-guidelines",
30
+ description: "UI/UX design principles and web standards",
31
+ category: "design",
32
+ },
33
+ {
34
+ name: "Remotion Best Practices",
35
+ repo: "remotion-dev/skills",
36
+ skillPath: "skills/remotion-best-practices",
37
+ description: "Programmatic video creation with Remotion",
38
+ category: "media",
39
+ },
40
+ {
41
+ name: "UI/UX Pro Max",
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
+ category: "design",
46
+ },
47
+ {
48
+ name: "ElevenLabs TTS",
49
+ repo: "inferen-sh/skills",
50
+ skillPath: "skills/elevenlabs-tts",
51
+ description: "Text-to-speech with ElevenLabs API integration",
52
+ category: "media",
53
+ },
54
+ {
55
+ name: "Audit Website",
56
+ repo: "squirrelscan/skills",
57
+ skillPath: "skills/audit-website",
58
+ description: "Security and quality auditing for web applications",
59
+ category: "security",
60
+ },
61
+ {
62
+ name: "Systematic Debugging",
63
+ repo: "obra/superpowers",
64
+ skillPath: "skills/systematic-debugging",
65
+ description: "Structured debugging methodology with root cause analysis",
66
+ category: "debugging",
67
+ },
68
+ {
69
+ name: "shadcn/ui",
70
+ repo: "shadcn-ui/ui",
71
+ skillPath: "packages/shadcn",
72
+ description: "shadcn/ui component library patterns and usage",
73
+ category: "frontend",
74
+ },
75
+ {
76
+ name: "Neon Postgres",
77
+ repo: "neondatabase/agent-skills",
78
+ skillPath: "skills/neon-postgres",
79
+ description: "Neon serverless Postgres setup and best practices",
80
+ category: "database",
81
+ },
82
+ {
83
+ name: "Neon Serverless",
84
+ repo: "neondatabase/ai-rules",
85
+ skillPath: "skills/neon-serverless",
86
+ description: "Serverless database patterns with Neon",
87
+ category: "database",
88
+ },
89
+ ];
90
+
91
+ export const DEFAULT_SKILL_REPOS: SkillSource[] = [
92
+ {
93
+ label: "Vercel Agent Skills",
94
+ repo: "vercel-labs/agent-skills",
95
+ skillsPath: "skills",
96
+ },
97
+ ];
@@ -0,0 +1,307 @@
1
+ import fs from "fs-extra";
2
+ import path from "node:path";
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";
6
+ const treeCache = new Map();
7
+ // ─── Path helpers ──────────────────────────────────────────────────────────────
8
+ export function getUserSkillsDir() {
9
+ return path.join(os.homedir(), ".claude", "skills");
10
+ }
11
+ export function getProjectSkillsDir(projectPath) {
12
+ return path.join(projectPath ?? process.cwd(), ".claude", "skills");
13
+ }
14
+ // ─── GitHub Tree API ──────────────────────────────────────────────────────────
15
+ async function fetchGitTree(repo) {
16
+ const cached = treeCache.get(repo);
17
+ const url = `https://api.github.com/repos/${repo}/git/trees/HEAD?recursive=1`;
18
+ const headers = {
19
+ Accept: "application/vnd.github+json",
20
+ "X-GitHub-Api-Version": "2022-11-28",
21
+ };
22
+ if (cached?.etag) {
23
+ headers["If-None-Match"] = cached.etag;
24
+ }
25
+ const token = process.env.GITHUB_TOKEN || process.env.GITHUB_PERSONAL_ACCESS_TOKEN;
26
+ if (token) {
27
+ headers.Authorization = `Bearer ${token}`;
28
+ }
29
+ const response = await fetch(url, {
30
+ headers,
31
+ signal: AbortSignal.timeout(10000),
32
+ });
33
+ if (response.status === 304 && cached) {
34
+ return cached.tree;
35
+ }
36
+ if (response.status === 403 || response.status === 429) {
37
+ const resetHeader = response.headers.get("X-RateLimit-Reset");
38
+ const resetTime = resetHeader
39
+ ? new Date(Number(resetHeader) * 1000).toLocaleTimeString()
40
+ : "unknown";
41
+ throw new Error(`GitHub API rate limit exceeded. Resets at ${resetTime}. Set GITHUB_TOKEN to increase limits.`);
42
+ }
43
+ if (!response.ok) {
44
+ throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
45
+ }
46
+ const etag = response.headers.get("ETag") || "";
47
+ const tree = (await response.json());
48
+ treeCache.set(repo, { etag, tree, fetchedAt: Date.now() });
49
+ return tree;
50
+ }
51
+ // ─── Frontmatter parser ───────────────────────────────────────────────────────
52
+ function parseYamlFrontmatter(content) {
53
+ const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
54
+ if (!frontmatterMatch)
55
+ return {};
56
+ const yaml = frontmatterMatch[1];
57
+ const result = {};
58
+ for (const line of yaml.split("\n")) {
59
+ const colonIdx = line.indexOf(":");
60
+ if (colonIdx === -1)
61
+ continue;
62
+ const key = line.slice(0, colonIdx).trim();
63
+ const rawValue = line.slice(colonIdx + 1).trim();
64
+ if (!key)
65
+ continue;
66
+ // Handle arrays (simple inline: [a, b, c])
67
+ if (rawValue.startsWith("[") && rawValue.endsWith("]")) {
68
+ const items = rawValue
69
+ .slice(1, -1)
70
+ .split(",")
71
+ .map((s) => s.trim().replace(/^["']|["']$/g, ""))
72
+ .filter(Boolean);
73
+ result[key] = items;
74
+ }
75
+ else {
76
+ // Strip quotes
77
+ result[key] = rawValue.replace(/^["']|["']$/g, "");
78
+ }
79
+ }
80
+ return result;
81
+ }
82
+ export async function fetchSkillFrontmatter(skill) {
83
+ const url = `https://raw.githubusercontent.com/${skill.source.repo}/HEAD/${skill.repoPath}`;
84
+ const response = await fetch(url, {
85
+ signal: AbortSignal.timeout(10000),
86
+ });
87
+ if (!response.ok) {
88
+ return {
89
+ name: skill.name,
90
+ description: "(no description)",
91
+ category: "general",
92
+ };
93
+ }
94
+ const content = await response.text();
95
+ const parsed = parseYamlFrontmatter(content);
96
+ return {
97
+ name: parsed.name || skill.name,
98
+ description: parsed.description || "(no description)",
99
+ category: parsed.category,
100
+ author: parsed.author,
101
+ version: parsed.version,
102
+ tags: parsed.tags,
103
+ };
104
+ }
105
+ // ─── Check installation ───────────────────────────────────────────────────────
106
+ export async function getInstalledSkillNames(scope, projectPath) {
107
+ const dir = scope === "user"
108
+ ? getUserSkillsDir()
109
+ : getProjectSkillsDir(projectPath);
110
+ const installed = new Set();
111
+ try {
112
+ if (!(await fs.pathExists(dir)))
113
+ return installed;
114
+ const entries = await fs.readdir(dir);
115
+ for (const entry of entries) {
116
+ const skillMd = path.join(dir, entry, "SKILL.md");
117
+ if (await fs.pathExists(skillMd)) {
118
+ installed.add(entry);
119
+ }
120
+ }
121
+ }
122
+ catch {
123
+ // ignore
124
+ }
125
+ return installed;
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
+ }
163
+ // ─── Fetch available skills ───────────────────────────────────────────────────
164
+ export async function fetchAvailableSkills(_repos, projectPath) {
165
+ const userInstalled = await getInstalledSkillNames("user");
166
+ const projectInstalled = await getInstalledSkillNames("project", projectPath);
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) {
245
+ try {
246
+ const response = await fetch(url, { signal: AbortSignal.timeout(8000) });
247
+ if (response.ok) {
248
+ content = await response.text();
249
+ break;
250
+ }
251
+ }
252
+ catch {
253
+ continue;
254
+ }
255
+ }
256
+ if (!content) {
257
+ throw new Error(`Failed to fetch skill: SKILL.md not found in ${repo}/${repoPath}`);
258
+ }
259
+ const installDir = scope === "user"
260
+ ? path.join(getUserSkillsDir(), skill.name)
261
+ : path.join(getProjectSkillsDir(projectPath), skill.name);
262
+ await fs.ensureDir(installDir);
263
+ await fs.writeFile(path.join(installDir, "SKILL.md"), content, "utf8");
264
+ }
265
+ export async function uninstallSkill(skillName, scope, projectPath) {
266
+ const installDir = scope === "user"
267
+ ? path.join(getUserSkillsDir(), skillName)
268
+ : path.join(getProjectSkillsDir(projectPath), skillName);
269
+ const skillMdPath = path.join(installDir, "SKILL.md");
270
+ if (await fs.pathExists(skillMdPath)) {
271
+ await fs.remove(skillMdPath);
272
+ }
273
+ // Try to remove directory if empty
274
+ try {
275
+ await fs.rmdir(installDir);
276
+ }
277
+ catch {
278
+ // Ignore if not empty or doesn't exist
279
+ }
280
+ }
281
+ // ─── Check installed skills from file system ──────────────────────────────────
282
+ export async function getInstalledSkillsFromFs(projectPath) {
283
+ const result = [];
284
+ const userDir = getUserSkillsDir();
285
+ const projDir = getProjectSkillsDir(projectPath);
286
+ const scopedDirs = [
287
+ [userDir, "user"],
288
+ [projDir, "project"],
289
+ ];
290
+ for (const [dir, scope] of scopedDirs) {
291
+ try {
292
+ if (!(await fs.pathExists(dir)))
293
+ continue;
294
+ const entries = await fs.readdir(dir);
295
+ for (const entry of entries) {
296
+ const skillMd = path.join(dir, entry, "SKILL.md");
297
+ if (await fs.pathExists(skillMd)) {
298
+ result.push({ name: entry, scope });
299
+ }
300
+ }
301
+ }
302
+ catch {
303
+ // ignore
304
+ }
305
+ }
306
+ return result;
307
+ }