claudeup 3.8.0 → 3.9.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 +86 -0
- package/src/data/skill-repos.ts +97 -0
- package/src/services/skills-manager.js +239 -0
- package/src/services/skills-manager.ts +328 -0
- package/src/services/skillsmp-client.js +67 -0
- package/src/services/skillsmp-client.ts +89 -0
- package/src/types/index.ts +67 -1
- package/src/ui/App.js +8 -2
- package/src/ui/App.tsx +7 -1
- package/src/ui/components/TabBar.js +1 -0
- package/src/ui/components/TabBar.tsx +1 -0
- package/src/ui/screens/SkillsScreen.js +325 -0
- package/src/ui/screens/SkillsScreen.tsx +574 -0
- package/src/ui/screens/index.js +1 -0
- package/src/ui/screens/index.ts +1 -0
- package/src/ui/state/reducer.js +88 -0
- package/src/ui/state/reducer.ts +99 -0
- package/src/ui/state/types.ts +27 -2
package/package.json
CHANGED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export const RECOMMENDED_SKILLS = [
|
|
2
|
+
{
|
|
3
|
+
name: "Find Skills",
|
|
4
|
+
repo: "vercel-labs/skills",
|
|
5
|
+
skillPath: "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: "vercel-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: "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: "remotion-best-practices",
|
|
27
|
+
description: "Programmatic video creation with Remotion",
|
|
28
|
+
category: "media",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
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",
|
|
35
|
+
category: "design",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "ElevenLabs TTS",
|
|
39
|
+
repo: "inferen-sh/skills",
|
|
40
|
+
skillPath: "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: "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: "systematic-debugging",
|
|
55
|
+
description: "Structured debugging methodology with root cause analysis",
|
|
56
|
+
category: "debugging",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "shadcn/ui",
|
|
60
|
+
repo: "shadcn/ui",
|
|
61
|
+
skillPath: "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: "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: "neon-serverless",
|
|
76
|
+
description: "Serverless database patterns with Neon",
|
|
77
|
+
category: "database",
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
export const DEFAULT_SKILL_REPOS = [
|
|
81
|
+
{
|
|
82
|
+
label: "vercel-labs/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: "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: "vercel-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: "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: "remotion-best-practices",
|
|
37
|
+
description: "Programmatic video creation with Remotion",
|
|
38
|
+
category: "media",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
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",
|
|
45
|
+
category: "design",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: "ElevenLabs TTS",
|
|
49
|
+
repo: "inferen-sh/skills",
|
|
50
|
+
skillPath: "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: "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: "systematic-debugging",
|
|
65
|
+
description: "Structured debugging methodology with root cause analysis",
|
|
66
|
+
category: "debugging",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: "shadcn/ui",
|
|
70
|
+
repo: "shadcn/ui",
|
|
71
|
+
skillPath: "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: "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: "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-labs/agent-skills",
|
|
94
|
+
repo: "vercel-labs/agent-skills",
|
|
95
|
+
skillsPath: "skills",
|
|
96
|
+
},
|
|
97
|
+
];
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
const treeCache = new Map();
|
|
5
|
+
// ─── Path helpers ──────────────────────────────────────────────────────────────
|
|
6
|
+
export function getUserSkillsDir() {
|
|
7
|
+
return path.join(os.homedir(), ".claude", "skills");
|
|
8
|
+
}
|
|
9
|
+
export function getProjectSkillsDir(projectPath) {
|
|
10
|
+
return path.join(projectPath ?? process.cwd(), ".claude", "skills");
|
|
11
|
+
}
|
|
12
|
+
// ─── GitHub Tree API ──────────────────────────────────────────────────────────
|
|
13
|
+
async function fetchGitTree(repo) {
|
|
14
|
+
const cached = treeCache.get(repo);
|
|
15
|
+
const url = `https://api.github.com/repos/${repo}/git/trees/HEAD?recursive=1`;
|
|
16
|
+
const headers = {
|
|
17
|
+
Accept: "application/vnd.github+json",
|
|
18
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
19
|
+
};
|
|
20
|
+
if (cached?.etag) {
|
|
21
|
+
headers["If-None-Match"] = cached.etag;
|
|
22
|
+
}
|
|
23
|
+
const token = process.env.GITHUB_TOKEN || process.env.GITHUB_PERSONAL_ACCESS_TOKEN;
|
|
24
|
+
if (token) {
|
|
25
|
+
headers.Authorization = `Bearer ${token}`;
|
|
26
|
+
}
|
|
27
|
+
const response = await fetch(url, {
|
|
28
|
+
headers,
|
|
29
|
+
signal: AbortSignal.timeout(10000),
|
|
30
|
+
});
|
|
31
|
+
if (response.status === 304 && cached) {
|
|
32
|
+
return cached.tree;
|
|
33
|
+
}
|
|
34
|
+
if (response.status === 403 || response.status === 429) {
|
|
35
|
+
const resetHeader = response.headers.get("X-RateLimit-Reset");
|
|
36
|
+
const resetTime = resetHeader
|
|
37
|
+
? new Date(Number(resetHeader) * 1000).toLocaleTimeString()
|
|
38
|
+
: "unknown";
|
|
39
|
+
throw new Error(`GitHub API rate limit exceeded. Resets at ${resetTime}. Set GITHUB_TOKEN to increase limits.`);
|
|
40
|
+
}
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
|
|
43
|
+
}
|
|
44
|
+
const etag = response.headers.get("ETag") || "";
|
|
45
|
+
const tree = (await response.json());
|
|
46
|
+
treeCache.set(repo, { etag, tree, fetchedAt: Date.now() });
|
|
47
|
+
return tree;
|
|
48
|
+
}
|
|
49
|
+
// ─── Frontmatter parser ───────────────────────────────────────────────────────
|
|
50
|
+
function parseYamlFrontmatter(content) {
|
|
51
|
+
const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
52
|
+
if (!frontmatterMatch)
|
|
53
|
+
return {};
|
|
54
|
+
const yaml = frontmatterMatch[1];
|
|
55
|
+
const result = {};
|
|
56
|
+
for (const line of yaml.split("\n")) {
|
|
57
|
+
const colonIdx = line.indexOf(":");
|
|
58
|
+
if (colonIdx === -1)
|
|
59
|
+
continue;
|
|
60
|
+
const key = line.slice(0, colonIdx).trim();
|
|
61
|
+
const rawValue = line.slice(colonIdx + 1).trim();
|
|
62
|
+
if (!key)
|
|
63
|
+
continue;
|
|
64
|
+
// Handle arrays (simple inline: [a, b, c])
|
|
65
|
+
if (rawValue.startsWith("[") && rawValue.endsWith("]")) {
|
|
66
|
+
const items = rawValue
|
|
67
|
+
.slice(1, -1)
|
|
68
|
+
.split(",")
|
|
69
|
+
.map((s) => s.trim().replace(/^["']|["']$/g, ""))
|
|
70
|
+
.filter(Boolean);
|
|
71
|
+
result[key] = items;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
// Strip quotes
|
|
75
|
+
result[key] = rawValue.replace(/^["']|["']$/g, "");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
export async function fetchSkillFrontmatter(skill) {
|
|
81
|
+
const url = `https://raw.githubusercontent.com/${skill.source.repo}/HEAD/${skill.repoPath}`;
|
|
82
|
+
const response = await fetch(url, {
|
|
83
|
+
signal: AbortSignal.timeout(10000),
|
|
84
|
+
});
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
return {
|
|
87
|
+
name: skill.name,
|
|
88
|
+
description: "(no description)",
|
|
89
|
+
category: "general",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const content = await response.text();
|
|
93
|
+
const parsed = parseYamlFrontmatter(content);
|
|
94
|
+
return {
|
|
95
|
+
name: parsed.name || skill.name,
|
|
96
|
+
description: parsed.description || "(no description)",
|
|
97
|
+
category: parsed.category,
|
|
98
|
+
author: parsed.author,
|
|
99
|
+
version: parsed.version,
|
|
100
|
+
tags: parsed.tags,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
// ─── Check installation ───────────────────────────────────────────────────────
|
|
104
|
+
export async function getInstalledSkillNames(scope, projectPath) {
|
|
105
|
+
const dir = scope === "user"
|
|
106
|
+
? getUserSkillsDir()
|
|
107
|
+
: getProjectSkillsDir(projectPath);
|
|
108
|
+
const installed = new Set();
|
|
109
|
+
try {
|
|
110
|
+
if (!(await fs.pathExists(dir)))
|
|
111
|
+
return installed;
|
|
112
|
+
const entries = await fs.readdir(dir);
|
|
113
|
+
for (const entry of entries) {
|
|
114
|
+
const skillMd = path.join(dir, entry, "SKILL.md");
|
|
115
|
+
if (await fs.pathExists(skillMd)) {
|
|
116
|
+
installed.add(entry);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// ignore
|
|
122
|
+
}
|
|
123
|
+
return installed;
|
|
124
|
+
}
|
|
125
|
+
// ─── Fetch available skills ───────────────────────────────────────────────────
|
|
126
|
+
export async function fetchAvailableSkills(repos, projectPath) {
|
|
127
|
+
const userInstalled = await getInstalledSkillNames("user");
|
|
128
|
+
const projectInstalled = await getInstalledSkillNames("project", projectPath);
|
|
129
|
+
const skills = [];
|
|
130
|
+
for (const source of repos) {
|
|
131
|
+
let tree;
|
|
132
|
+
try {
|
|
133
|
+
tree = await fetchGitTree(source.repo);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// Skip this repo on error
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
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
|
+
}
|
|
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}`);
|
|
189
|
+
}
|
|
190
|
+
const content = await response.text();
|
|
191
|
+
const installDir = scope === "user"
|
|
192
|
+
? path.join(getUserSkillsDir(), skill.name)
|
|
193
|
+
: path.join(getProjectSkillsDir(projectPath), skill.name);
|
|
194
|
+
await fs.ensureDir(installDir);
|
|
195
|
+
await fs.writeFile(path.join(installDir, "SKILL.md"), content, "utf8");
|
|
196
|
+
}
|
|
197
|
+
export async function uninstallSkill(skillName, scope, projectPath) {
|
|
198
|
+
const installDir = scope === "user"
|
|
199
|
+
? path.join(getUserSkillsDir(), skillName)
|
|
200
|
+
: path.join(getProjectSkillsDir(projectPath), skillName);
|
|
201
|
+
const skillMdPath = path.join(installDir, "SKILL.md");
|
|
202
|
+
if (await fs.pathExists(skillMdPath)) {
|
|
203
|
+
await fs.remove(skillMdPath);
|
|
204
|
+
}
|
|
205
|
+
// Try to remove directory if empty
|
|
206
|
+
try {
|
|
207
|
+
await fs.rmdir(installDir);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// Ignore if not empty or doesn't exist
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// ─── Check installed skills from file system ──────────────────────────────────
|
|
214
|
+
export async function getInstalledSkillsFromFs(projectPath) {
|
|
215
|
+
const result = [];
|
|
216
|
+
const userDir = getUserSkillsDir();
|
|
217
|
+
const projDir = getProjectSkillsDir(projectPath);
|
|
218
|
+
const scopedDirs = [
|
|
219
|
+
[userDir, "user"],
|
|
220
|
+
[projDir, "project"],
|
|
221
|
+
];
|
|
222
|
+
for (const [dir, scope] of scopedDirs) {
|
|
223
|
+
try {
|
|
224
|
+
if (!(await fs.pathExists(dir)))
|
|
225
|
+
continue;
|
|
226
|
+
const entries = await fs.readdir(dir);
|
|
227
|
+
for (const entry of entries) {
|
|
228
|
+
const skillMd = path.join(dir, entry, "SKILL.md");
|
|
229
|
+
if (await fs.pathExists(skillMd)) {
|
|
230
|
+
result.push({ name: entry, scope });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
// ignore
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return result;
|
|
239
|
+
}
|