@wingman-ai/gateway 0.3.2 → 0.4.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/dist/agent/config/mcpClientManager.cjs +48 -9
- package/dist/agent/config/mcpClientManager.d.ts +12 -0
- package/dist/agent/config/mcpClientManager.js +48 -9
- package/dist/agent/tests/mcpClientManager.test.cjs +50 -0
- package/dist/agent/tests/mcpClientManager.test.js +50 -0
- package/dist/cli/commands/skill.cjs +12 -4
- package/dist/cli/commands/skill.js +12 -4
- package/dist/cli/config/jsonSchema.cjs +55 -0
- package/dist/cli/config/jsonSchema.d.ts +2 -0
- package/dist/cli/config/jsonSchema.js +18 -0
- package/dist/cli/config/loader.cjs +33 -1
- package/dist/cli/config/loader.js +33 -1
- package/dist/cli/config/schema.cjs +119 -2
- package/dist/cli/config/schema.d.ts +40 -0
- package/dist/cli/config/schema.js +119 -2
- package/dist/cli/core/agentInvoker.cjs +4 -1
- package/dist/cli/core/agentInvoker.d.ts +3 -0
- package/dist/cli/core/agentInvoker.js +4 -1
- package/dist/cli/services/skillRepository.cjs +138 -20
- package/dist/cli/services/skillRepository.d.ts +10 -2
- package/dist/cli/services/skillRepository.js +138 -20
- package/dist/cli/services/skillSecurityScanner.cjs +158 -0
- package/dist/cli/services/skillSecurityScanner.d.ts +28 -0
- package/dist/cli/services/skillSecurityScanner.js +121 -0
- package/dist/cli/services/skillService.cjs +44 -12
- package/dist/cli/services/skillService.d.ts +2 -0
- package/dist/cli/services/skillService.js +46 -14
- package/dist/cli/types/skill.d.ts +9 -0
- package/dist/gateway/server.cjs +5 -1
- package/dist/gateway/server.js +5 -1
- package/dist/gateway/types.d.ts +9 -0
- package/dist/tests/cli-config-loader.test.cjs +33 -1
- package/dist/tests/cli-config-loader.test.js +33 -1
- package/dist/tests/config-json-schema.test.cjs +25 -0
- package/dist/tests/config-json-schema.test.d.ts +1 -0
- package/dist/tests/config-json-schema.test.js +19 -0
- package/dist/tests/skill-repository.test.cjs +106 -0
- package/dist/tests/skill-repository.test.d.ts +1 -0
- package/dist/tests/skill-repository.test.js +100 -0
- package/dist/tests/skill-security-scanner.test.cjs +126 -0
- package/dist/tests/skill-security-scanner.test.d.ts +1 -0
- package/dist/tests/skill-security-scanner.test.js +120 -0
- package/dist/tests/uv.test.cjs +47 -0
- package/dist/tests/uv.test.d.ts +1 -0
- package/dist/tests/uv.test.js +41 -0
- package/dist/utils/uv.cjs +64 -0
- package/dist/utils/uv.d.ts +3 -0
- package/dist/utils/uv.js +24 -0
- package/package.json +2 -1
- package/skills/gog/SKILL.md +36 -0
- package/skills/weather/SKILL.md +49 -0
|
@@ -39,8 +39,8 @@ function _define_property(obj, key, value) {
|
|
|
39
39
|
}
|
|
40
40
|
const logger = (0, external_logger_cjs_namespaceObject.createLogger)();
|
|
41
41
|
class SkillRepository {
|
|
42
|
-
async
|
|
43
|
-
const url = `${this.
|
|
42
|
+
async fetchGitHub(path) {
|
|
43
|
+
const url = `${this.githubBaseUrl}${path}`;
|
|
44
44
|
const headers = {
|
|
45
45
|
Accept: "application/vnd.github.v3+json",
|
|
46
46
|
"User-Agent": "wingman-cli"
|
|
@@ -62,29 +62,33 @@ class SkillRepository {
|
|
|
62
62
|
}
|
|
63
63
|
async listAvailableSkills() {
|
|
64
64
|
try {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
for (const item of contents)if ("dir" === item.type) try {
|
|
68
|
-
const metadata = await this.getSkillMetadata(item.name);
|
|
69
|
-
skills.push({
|
|
70
|
-
name: item.name,
|
|
71
|
-
description: metadata.description || "No description",
|
|
72
|
-
path: item.path,
|
|
73
|
-
metadata
|
|
74
|
-
});
|
|
75
|
-
} catch (error) {
|
|
76
|
-
logger.warn(`Could not read skill ${item.name}`, error instanceof Error ? error.message : String(error));
|
|
77
|
-
}
|
|
78
|
-
return skills;
|
|
65
|
+
if ("clawhub" === this.provider) return await this.listSkillsFromClawhub();
|
|
66
|
+
return await this.listSkillsFromGitHub();
|
|
79
67
|
} catch (error) {
|
|
80
68
|
if (error instanceof Error) throw new Error(`Failed to list skills: ${error.message}`);
|
|
81
69
|
throw error;
|
|
82
70
|
}
|
|
83
71
|
}
|
|
72
|
+
async fetchJson(url, options) {
|
|
73
|
+
const response = await fetch(url, {
|
|
74
|
+
headers: options?.headers
|
|
75
|
+
});
|
|
76
|
+
if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
77
|
+
return await response.json();
|
|
78
|
+
}
|
|
79
|
+
async fetchBinary(url, options) {
|
|
80
|
+
const response = await fetch(url, {
|
|
81
|
+
headers: options?.headers
|
|
82
|
+
});
|
|
83
|
+
if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
84
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
85
|
+
return Buffer.from(arrayBuffer);
|
|
86
|
+
}
|
|
84
87
|
async getSkillMetadata(skillName) {
|
|
88
|
+
if ("clawhub" === this.provider) return await this.getClawhubSkillMetadata(skillName);
|
|
85
89
|
try {
|
|
86
90
|
const skillMdPath = `/repos/${this.owner}/${this.repo}/contents/skills/${skillName}/SKILL.md`;
|
|
87
|
-
const skillMd = await this.
|
|
91
|
+
const skillMd = await this.fetchGitHub(skillMdPath);
|
|
88
92
|
if ("file" !== skillMd.type || !skillMd.content) throw new Error("SKILL.md not found or invalid");
|
|
89
93
|
const content = Buffer.from(skillMd.content, "base64").toString("utf-8");
|
|
90
94
|
const metadata = this.parseSkillMetadata(content);
|
|
@@ -94,6 +98,35 @@ class SkillRepository {
|
|
|
94
98
|
throw error;
|
|
95
99
|
}
|
|
96
100
|
}
|
|
101
|
+
async getClawhubSkillMetadata(skillName) {
|
|
102
|
+
try {
|
|
103
|
+
const detail = await this.fetchJson(`${this.clawhubBaseUrl}/api/v1/skills/${encodeURIComponent(skillName)}`, {
|
|
104
|
+
headers: {
|
|
105
|
+
Accept: "application/json",
|
|
106
|
+
"User-Agent": "wingman-cli"
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
const slug = detail.skill?.slug?.trim() || skillName.trim();
|
|
110
|
+
const description = detail.skill?.summary?.trim() || detail.skill?.displayName?.trim() || "No description";
|
|
111
|
+
const nameRegex = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
112
|
+
if (!nameRegex.test(slug)) throw new Error(`Invalid skill name '${slug}': must be lowercase alphanumeric with hyphens only`);
|
|
113
|
+
return {
|
|
114
|
+
name: slug,
|
|
115
|
+
description,
|
|
116
|
+
metadata: {
|
|
117
|
+
...detail.latestVersion?.version ? {
|
|
118
|
+
version: detail.latestVersion.version
|
|
119
|
+
} : {},
|
|
120
|
+
...detail.owner?.handle ? {
|
|
121
|
+
owner: detail.owner.handle
|
|
122
|
+
} : {}
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (error instanceof Error) throw new Error(`Failed to fetch skill metadata for ${skillName}: ${error.message}`);
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
97
130
|
parseSkillMetadata(content) {
|
|
98
131
|
const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---/;
|
|
99
132
|
const match = content.match(frontmatterRegex);
|
|
@@ -134,6 +167,7 @@ class SkillRepository {
|
|
|
134
167
|
return metadata;
|
|
135
168
|
}
|
|
136
169
|
async downloadSkill(skillName) {
|
|
170
|
+
if ("clawhub" === this.provider) return await this.downloadSkillFromClawhub(skillName);
|
|
137
171
|
try {
|
|
138
172
|
const files = new Map();
|
|
139
173
|
await this.downloadDirectory(`skills/${skillName}`, files, skillName);
|
|
@@ -143,14 +177,51 @@ class SkillRepository {
|
|
|
143
177
|
throw error;
|
|
144
178
|
}
|
|
145
179
|
}
|
|
180
|
+
async downloadSkillFromClawhub(skillName) {
|
|
181
|
+
try {
|
|
182
|
+
const detail = await this.fetchJson(`${this.clawhubBaseUrl}/api/v1/skills/${encodeURIComponent(skillName)}`, {
|
|
183
|
+
headers: {
|
|
184
|
+
Accept: "application/json",
|
|
185
|
+
"User-Agent": "wingman-cli"
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
const slug = detail.skill?.slug || skillName;
|
|
189
|
+
const version = detail.latestVersion?.version;
|
|
190
|
+
if (!version) throw new Error("No latest version available");
|
|
191
|
+
const filesResponse = await this.fetchJson(`${this.clawhubBaseUrl}/api/v1/skills/${encodeURIComponent(slug)}/versions/${encodeURIComponent(version)}`, {
|
|
192
|
+
headers: {
|
|
193
|
+
Accept: "application/json",
|
|
194
|
+
"User-Agent": "wingman-cli"
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
const files = filesResponse.version?.files || [];
|
|
198
|
+
const output = new Map();
|
|
199
|
+
for (const file of files){
|
|
200
|
+
const fileUrl = new URL(`${this.clawhubBaseUrl}/api/v1/skills/${encodeURIComponent(slug)}/file`);
|
|
201
|
+
fileUrl.searchParams.set("path", file.path);
|
|
202
|
+
fileUrl.searchParams.set("version", version);
|
|
203
|
+
const content = await this.fetchBinary(fileUrl.toString(), {
|
|
204
|
+
headers: {
|
|
205
|
+
Accept: "text/plain, application/octet-stream",
|
|
206
|
+
"User-Agent": "wingman-cli"
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
output.set(file.path, content);
|
|
210
|
+
}
|
|
211
|
+
return output;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
if (error instanceof Error) throw new Error(`Failed to download skill ${skillName}: ${error.message}`);
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
146
217
|
async downloadDirectory(path, files, skillName) {
|
|
147
|
-
const contents = await this.
|
|
218
|
+
const contents = await this.fetchGitHub(`/repos/${this.owner}/${this.repo}/contents/${path}`);
|
|
148
219
|
for (const item of contents)if ("file" === item.type) if (item.content) {
|
|
149
220
|
const content = Buffer.from(item.content, "base64");
|
|
150
221
|
const relativePath = item.path.replace(`skills/${skillName}/`, "");
|
|
151
222
|
files.set(relativePath, content);
|
|
152
223
|
} else {
|
|
153
|
-
const fileData = await this.
|
|
224
|
+
const fileData = await this.fetchGitHub(item.url.replace(this.githubBaseUrl, ""));
|
|
154
225
|
if (fileData.content && "base64" === fileData.encoding) {
|
|
155
226
|
const content = Buffer.from(fileData.content, "base64");
|
|
156
227
|
const relativePath = item.path.replace(`skills/${skillName}/`, "");
|
|
@@ -159,14 +230,61 @@ class SkillRepository {
|
|
|
159
230
|
}
|
|
160
231
|
else if ("dir" === item.type) await this.downloadDirectory(item.path, files, skillName);
|
|
161
232
|
}
|
|
233
|
+
async listSkillsFromClawhub() {
|
|
234
|
+
const allSkills = [];
|
|
235
|
+
let cursor = null;
|
|
236
|
+
do {
|
|
237
|
+
const url = new URL(`${this.clawhubBaseUrl}/api/v1/skills`);
|
|
238
|
+
url.searchParams.set("sort", "downloads");
|
|
239
|
+
url.searchParams.set("limit", "100");
|
|
240
|
+
if (cursor) url.searchParams.set("cursor", cursor);
|
|
241
|
+
const response = await this.fetchJson(url.toString(), {
|
|
242
|
+
headers: {
|
|
243
|
+
Accept: "application/json",
|
|
244
|
+
"User-Agent": "wingman-cli"
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
for (const item of response.items || [])allSkills.push({
|
|
248
|
+
name: item.slug,
|
|
249
|
+
description: item.summary?.trim() || item.displayName?.trim() || "No description",
|
|
250
|
+
path: item.slug,
|
|
251
|
+
metadata: {
|
|
252
|
+
name: item.slug,
|
|
253
|
+
description: item.summary?.trim() || item.displayName?.trim() || "No description"
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
cursor = response.nextCursor || null;
|
|
257
|
+
}while (cursor);
|
|
258
|
+
return allSkills;
|
|
259
|
+
}
|
|
260
|
+
async listSkillsFromGitHub() {
|
|
261
|
+
const contents = await this.fetchGitHub(`/repos/${this.owner}/${this.repo}/contents/skills`);
|
|
262
|
+
const skills = [];
|
|
263
|
+
for (const item of contents)if ("dir" === item.type) try {
|
|
264
|
+
const metadata = await this.getSkillMetadata(item.name);
|
|
265
|
+
skills.push({
|
|
266
|
+
name: item.name,
|
|
267
|
+
description: metadata.description || "No description",
|
|
268
|
+
path: item.path,
|
|
269
|
+
metadata
|
|
270
|
+
});
|
|
271
|
+
} catch (error) {
|
|
272
|
+
logger.warn(`Could not read skill ${item.name}`, error instanceof Error ? error.message : String(error));
|
|
273
|
+
}
|
|
274
|
+
return skills;
|
|
275
|
+
}
|
|
162
276
|
constructor(options = {}){
|
|
163
|
-
_define_property(this, "
|
|
277
|
+
_define_property(this, "githubBaseUrl", "https://api.github.com");
|
|
164
278
|
_define_property(this, "owner", void 0);
|
|
165
279
|
_define_property(this, "repo", void 0);
|
|
166
280
|
_define_property(this, "token", void 0);
|
|
281
|
+
_define_property(this, "provider", void 0);
|
|
282
|
+
_define_property(this, "clawhubBaseUrl", void 0);
|
|
283
|
+
this.provider = options.provider || "github";
|
|
167
284
|
this.owner = options.repositoryOwner || "anthropics";
|
|
168
285
|
this.repo = options.repositoryName || "skills";
|
|
169
286
|
this.token = options.githubToken || process.env.GITHUB_TOKEN || void 0;
|
|
287
|
+
this.clawhubBaseUrl = (options.clawhubBaseUrl || "https://clawhub.ai").replace(/\/+$/, "");
|
|
170
288
|
}
|
|
171
289
|
}
|
|
172
290
|
exports.SkillRepository = __webpack_exports__.SkillRepository;
|
|
@@ -3,23 +3,28 @@ import type { SkillInfo, SkillMetadata, SkillRepositoryOptions } from "../types/
|
|
|
3
3
|
* GitHub API client for interacting with the skills repository
|
|
4
4
|
*/
|
|
5
5
|
export declare class SkillRepository {
|
|
6
|
-
private readonly
|
|
6
|
+
private readonly githubBaseUrl;
|
|
7
7
|
private readonly owner;
|
|
8
8
|
private readonly repo;
|
|
9
9
|
private readonly token?;
|
|
10
|
+
private readonly provider;
|
|
11
|
+
private readonly clawhubBaseUrl;
|
|
10
12
|
constructor(options?: SkillRepositoryOptions);
|
|
11
13
|
/**
|
|
12
14
|
* Fetch data from GitHub API
|
|
13
15
|
*/
|
|
14
|
-
private
|
|
16
|
+
private fetchGitHub;
|
|
15
17
|
/**
|
|
16
18
|
* List available skills from the repository
|
|
17
19
|
*/
|
|
18
20
|
listAvailableSkills(): Promise<SkillInfo[]>;
|
|
21
|
+
private fetchJson;
|
|
22
|
+
private fetchBinary;
|
|
19
23
|
/**
|
|
20
24
|
* Get skill metadata by fetching and parsing SKILL.md
|
|
21
25
|
*/
|
|
22
26
|
getSkillMetadata(skillName: string): Promise<SkillMetadata>;
|
|
27
|
+
private getClawhubSkillMetadata;
|
|
23
28
|
/**
|
|
24
29
|
* Parse SKILL.md content to extract YAML frontmatter
|
|
25
30
|
*/
|
|
@@ -28,8 +33,11 @@ export declare class SkillRepository {
|
|
|
28
33
|
* Download all files for a skill
|
|
29
34
|
*/
|
|
30
35
|
downloadSkill(skillName: string): Promise<Map<string, string | Buffer>>;
|
|
36
|
+
private downloadSkillFromClawhub;
|
|
31
37
|
/**
|
|
32
38
|
* Recursively download all files in a directory
|
|
33
39
|
*/
|
|
34
40
|
private downloadDirectory;
|
|
41
|
+
private listSkillsFromClawhub;
|
|
42
|
+
private listSkillsFromGitHub;
|
|
35
43
|
}
|
|
@@ -11,8 +11,8 @@ function _define_property(obj, key, value) {
|
|
|
11
11
|
}
|
|
12
12
|
const logger = createLogger();
|
|
13
13
|
class SkillRepository {
|
|
14
|
-
async
|
|
15
|
-
const url = `${this.
|
|
14
|
+
async fetchGitHub(path) {
|
|
15
|
+
const url = `${this.githubBaseUrl}${path}`;
|
|
16
16
|
const headers = {
|
|
17
17
|
Accept: "application/vnd.github.v3+json",
|
|
18
18
|
"User-Agent": "wingman-cli"
|
|
@@ -34,29 +34,33 @@ class SkillRepository {
|
|
|
34
34
|
}
|
|
35
35
|
async listAvailableSkills() {
|
|
36
36
|
try {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
for (const item of contents)if ("dir" === item.type) try {
|
|
40
|
-
const metadata = await this.getSkillMetadata(item.name);
|
|
41
|
-
skills.push({
|
|
42
|
-
name: item.name,
|
|
43
|
-
description: metadata.description || "No description",
|
|
44
|
-
path: item.path,
|
|
45
|
-
metadata
|
|
46
|
-
});
|
|
47
|
-
} catch (error) {
|
|
48
|
-
logger.warn(`Could not read skill ${item.name}`, error instanceof Error ? error.message : String(error));
|
|
49
|
-
}
|
|
50
|
-
return skills;
|
|
37
|
+
if ("clawhub" === this.provider) return await this.listSkillsFromClawhub();
|
|
38
|
+
return await this.listSkillsFromGitHub();
|
|
51
39
|
} catch (error) {
|
|
52
40
|
if (error instanceof Error) throw new Error(`Failed to list skills: ${error.message}`);
|
|
53
41
|
throw error;
|
|
54
42
|
}
|
|
55
43
|
}
|
|
44
|
+
async fetchJson(url, options) {
|
|
45
|
+
const response = await fetch(url, {
|
|
46
|
+
headers: options?.headers
|
|
47
|
+
});
|
|
48
|
+
if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
49
|
+
return await response.json();
|
|
50
|
+
}
|
|
51
|
+
async fetchBinary(url, options) {
|
|
52
|
+
const response = await fetch(url, {
|
|
53
|
+
headers: options?.headers
|
|
54
|
+
});
|
|
55
|
+
if (!response.ok) throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
56
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
57
|
+
return Buffer.from(arrayBuffer);
|
|
58
|
+
}
|
|
56
59
|
async getSkillMetadata(skillName) {
|
|
60
|
+
if ("clawhub" === this.provider) return await this.getClawhubSkillMetadata(skillName);
|
|
57
61
|
try {
|
|
58
62
|
const skillMdPath = `/repos/${this.owner}/${this.repo}/contents/skills/${skillName}/SKILL.md`;
|
|
59
|
-
const skillMd = await this.
|
|
63
|
+
const skillMd = await this.fetchGitHub(skillMdPath);
|
|
60
64
|
if ("file" !== skillMd.type || !skillMd.content) throw new Error("SKILL.md not found or invalid");
|
|
61
65
|
const content = Buffer.from(skillMd.content, "base64").toString("utf-8");
|
|
62
66
|
const metadata = this.parseSkillMetadata(content);
|
|
@@ -66,6 +70,35 @@ class SkillRepository {
|
|
|
66
70
|
throw error;
|
|
67
71
|
}
|
|
68
72
|
}
|
|
73
|
+
async getClawhubSkillMetadata(skillName) {
|
|
74
|
+
try {
|
|
75
|
+
const detail = await this.fetchJson(`${this.clawhubBaseUrl}/api/v1/skills/${encodeURIComponent(skillName)}`, {
|
|
76
|
+
headers: {
|
|
77
|
+
Accept: "application/json",
|
|
78
|
+
"User-Agent": "wingman-cli"
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
const slug = detail.skill?.slug?.trim() || skillName.trim();
|
|
82
|
+
const description = detail.skill?.summary?.trim() || detail.skill?.displayName?.trim() || "No description";
|
|
83
|
+
const nameRegex = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
84
|
+
if (!nameRegex.test(slug)) throw new Error(`Invalid skill name '${slug}': must be lowercase alphanumeric with hyphens only`);
|
|
85
|
+
return {
|
|
86
|
+
name: slug,
|
|
87
|
+
description,
|
|
88
|
+
metadata: {
|
|
89
|
+
...detail.latestVersion?.version ? {
|
|
90
|
+
version: detail.latestVersion.version
|
|
91
|
+
} : {},
|
|
92
|
+
...detail.owner?.handle ? {
|
|
93
|
+
owner: detail.owner.handle
|
|
94
|
+
} : {}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (error instanceof Error) throw new Error(`Failed to fetch skill metadata for ${skillName}: ${error.message}`);
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
69
102
|
parseSkillMetadata(content) {
|
|
70
103
|
const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---/;
|
|
71
104
|
const match = content.match(frontmatterRegex);
|
|
@@ -106,6 +139,7 @@ class SkillRepository {
|
|
|
106
139
|
return metadata;
|
|
107
140
|
}
|
|
108
141
|
async downloadSkill(skillName) {
|
|
142
|
+
if ("clawhub" === this.provider) return await this.downloadSkillFromClawhub(skillName);
|
|
109
143
|
try {
|
|
110
144
|
const files = new Map();
|
|
111
145
|
await this.downloadDirectory(`skills/${skillName}`, files, skillName);
|
|
@@ -115,14 +149,51 @@ class SkillRepository {
|
|
|
115
149
|
throw error;
|
|
116
150
|
}
|
|
117
151
|
}
|
|
152
|
+
async downloadSkillFromClawhub(skillName) {
|
|
153
|
+
try {
|
|
154
|
+
const detail = await this.fetchJson(`${this.clawhubBaseUrl}/api/v1/skills/${encodeURIComponent(skillName)}`, {
|
|
155
|
+
headers: {
|
|
156
|
+
Accept: "application/json",
|
|
157
|
+
"User-Agent": "wingman-cli"
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
const slug = detail.skill?.slug || skillName;
|
|
161
|
+
const version = detail.latestVersion?.version;
|
|
162
|
+
if (!version) throw new Error("No latest version available");
|
|
163
|
+
const filesResponse = await this.fetchJson(`${this.clawhubBaseUrl}/api/v1/skills/${encodeURIComponent(slug)}/versions/${encodeURIComponent(version)}`, {
|
|
164
|
+
headers: {
|
|
165
|
+
Accept: "application/json",
|
|
166
|
+
"User-Agent": "wingman-cli"
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
const files = filesResponse.version?.files || [];
|
|
170
|
+
const output = new Map();
|
|
171
|
+
for (const file of files){
|
|
172
|
+
const fileUrl = new URL(`${this.clawhubBaseUrl}/api/v1/skills/${encodeURIComponent(slug)}/file`);
|
|
173
|
+
fileUrl.searchParams.set("path", file.path);
|
|
174
|
+
fileUrl.searchParams.set("version", version);
|
|
175
|
+
const content = await this.fetchBinary(fileUrl.toString(), {
|
|
176
|
+
headers: {
|
|
177
|
+
Accept: "text/plain, application/octet-stream",
|
|
178
|
+
"User-Agent": "wingman-cli"
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
output.set(file.path, content);
|
|
182
|
+
}
|
|
183
|
+
return output;
|
|
184
|
+
} catch (error) {
|
|
185
|
+
if (error instanceof Error) throw new Error(`Failed to download skill ${skillName}: ${error.message}`);
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
118
189
|
async downloadDirectory(path, files, skillName) {
|
|
119
|
-
const contents = await this.
|
|
190
|
+
const contents = await this.fetchGitHub(`/repos/${this.owner}/${this.repo}/contents/${path}`);
|
|
120
191
|
for (const item of contents)if ("file" === item.type) if (item.content) {
|
|
121
192
|
const content = Buffer.from(item.content, "base64");
|
|
122
193
|
const relativePath = item.path.replace(`skills/${skillName}/`, "");
|
|
123
194
|
files.set(relativePath, content);
|
|
124
195
|
} else {
|
|
125
|
-
const fileData = await this.
|
|
196
|
+
const fileData = await this.fetchGitHub(item.url.replace(this.githubBaseUrl, ""));
|
|
126
197
|
if (fileData.content && "base64" === fileData.encoding) {
|
|
127
198
|
const content = Buffer.from(fileData.content, "base64");
|
|
128
199
|
const relativePath = item.path.replace(`skills/${skillName}/`, "");
|
|
@@ -131,14 +202,61 @@ class SkillRepository {
|
|
|
131
202
|
}
|
|
132
203
|
else if ("dir" === item.type) await this.downloadDirectory(item.path, files, skillName);
|
|
133
204
|
}
|
|
205
|
+
async listSkillsFromClawhub() {
|
|
206
|
+
const allSkills = [];
|
|
207
|
+
let cursor = null;
|
|
208
|
+
do {
|
|
209
|
+
const url = new URL(`${this.clawhubBaseUrl}/api/v1/skills`);
|
|
210
|
+
url.searchParams.set("sort", "downloads");
|
|
211
|
+
url.searchParams.set("limit", "100");
|
|
212
|
+
if (cursor) url.searchParams.set("cursor", cursor);
|
|
213
|
+
const response = await this.fetchJson(url.toString(), {
|
|
214
|
+
headers: {
|
|
215
|
+
Accept: "application/json",
|
|
216
|
+
"User-Agent": "wingman-cli"
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
for (const item of response.items || [])allSkills.push({
|
|
220
|
+
name: item.slug,
|
|
221
|
+
description: item.summary?.trim() || item.displayName?.trim() || "No description",
|
|
222
|
+
path: item.slug,
|
|
223
|
+
metadata: {
|
|
224
|
+
name: item.slug,
|
|
225
|
+
description: item.summary?.trim() || item.displayName?.trim() || "No description"
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
cursor = response.nextCursor || null;
|
|
229
|
+
}while (cursor);
|
|
230
|
+
return allSkills;
|
|
231
|
+
}
|
|
232
|
+
async listSkillsFromGitHub() {
|
|
233
|
+
const contents = await this.fetchGitHub(`/repos/${this.owner}/${this.repo}/contents/skills`);
|
|
234
|
+
const skills = [];
|
|
235
|
+
for (const item of contents)if ("dir" === item.type) try {
|
|
236
|
+
const metadata = await this.getSkillMetadata(item.name);
|
|
237
|
+
skills.push({
|
|
238
|
+
name: item.name,
|
|
239
|
+
description: metadata.description || "No description",
|
|
240
|
+
path: item.path,
|
|
241
|
+
metadata
|
|
242
|
+
});
|
|
243
|
+
} catch (error) {
|
|
244
|
+
logger.warn(`Could not read skill ${item.name}`, error instanceof Error ? error.message : String(error));
|
|
245
|
+
}
|
|
246
|
+
return skills;
|
|
247
|
+
}
|
|
134
248
|
constructor(options = {}){
|
|
135
|
-
_define_property(this, "
|
|
249
|
+
_define_property(this, "githubBaseUrl", "https://api.github.com");
|
|
136
250
|
_define_property(this, "owner", void 0);
|
|
137
251
|
_define_property(this, "repo", void 0);
|
|
138
252
|
_define_property(this, "token", void 0);
|
|
253
|
+
_define_property(this, "provider", void 0);
|
|
254
|
+
_define_property(this, "clawhubBaseUrl", void 0);
|
|
255
|
+
this.provider = options.provider || "github";
|
|
139
256
|
this.owner = options.repositoryOwner || "anthropics";
|
|
140
257
|
this.repo = options.repositoryName || "skills";
|
|
141
258
|
this.token = options.githubToken || process.env.GITHUB_TOKEN || void 0;
|
|
259
|
+
this.clawhubBaseUrl = (options.clawhubBaseUrl || "https://clawhub.ai").replace(/\/+$/, "");
|
|
142
260
|
}
|
|
143
261
|
}
|
|
144
262
|
export { SkillRepository };
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __webpack_require__ = {};
|
|
3
|
+
(()=>{
|
|
4
|
+
__webpack_require__.d = (exports1, definition)=>{
|
|
5
|
+
for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: definition[key]
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
})();
|
|
11
|
+
(()=>{
|
|
12
|
+
__webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
|
|
13
|
+
})();
|
|
14
|
+
(()=>{
|
|
15
|
+
__webpack_require__.r = (exports1)=>{
|
|
16
|
+
if ("u" > typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
|
|
17
|
+
value: 'Module'
|
|
18
|
+
});
|
|
19
|
+
Object.defineProperty(exports1, '__esModule', {
|
|
20
|
+
value: true
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
})();
|
|
24
|
+
var __webpack_exports__ = {};
|
|
25
|
+
__webpack_require__.r(__webpack_exports__);
|
|
26
|
+
__webpack_require__.d(__webpack_exports__, {
|
|
27
|
+
__skillSecurityScanner: ()=>__skillSecurityScanner,
|
|
28
|
+
scanSkillDirectory: ()=>scanSkillDirectory
|
|
29
|
+
});
|
|
30
|
+
const external_node_child_process_namespaceObject = require("node:child_process");
|
|
31
|
+
const uv_cjs_namespaceObject = require("../../utils/uv.cjs");
|
|
32
|
+
const DEFAULT_SCANNER_COMMAND = "uvx";
|
|
33
|
+
const DEFAULT_SCANNER_ARGS = [
|
|
34
|
+
"--from",
|
|
35
|
+
"mcp-scan>=0.4,<0.5",
|
|
36
|
+
"mcp-scan",
|
|
37
|
+
"--json",
|
|
38
|
+
"--skills"
|
|
39
|
+
];
|
|
40
|
+
const DEFAULT_BLOCKED_CODES = [
|
|
41
|
+
"MCP501",
|
|
42
|
+
"MCP506",
|
|
43
|
+
"MCP507",
|
|
44
|
+
"MCP508",
|
|
45
|
+
"MCP509",
|
|
46
|
+
"MCP510",
|
|
47
|
+
"MCP511"
|
|
48
|
+
];
|
|
49
|
+
function getScannerCommand(security) {
|
|
50
|
+
return security?.scannerCommand?.trim() || DEFAULT_SCANNER_COMMAND;
|
|
51
|
+
}
|
|
52
|
+
function getScannerArgs(security) {
|
|
53
|
+
if (Array.isArray(security?.scannerArgs) && security.scannerArgs.length > 0) return security.scannerArgs;
|
|
54
|
+
return DEFAULT_SCANNER_ARGS;
|
|
55
|
+
}
|
|
56
|
+
function getBlockedIssueCodes(security) {
|
|
57
|
+
const configured = security?.blockIssueCodes || DEFAULT_BLOCKED_CODES;
|
|
58
|
+
return new Set(configured.map((code)=>code.trim().toUpperCase()).filter(Boolean));
|
|
59
|
+
}
|
|
60
|
+
function parseScanResult(stdout) {
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(stdout);
|
|
63
|
+
} catch {
|
|
64
|
+
const firstBrace = stdout.indexOf("{");
|
|
65
|
+
const lastBrace = stdout.lastIndexOf("}");
|
|
66
|
+
if (-1 === firstBrace || -1 === lastBrace || lastBrace < firstBrace) throw new Error("Scanner output did not include JSON");
|
|
67
|
+
const jsonPayload = stdout.slice(firstBrace, lastBrace + 1);
|
|
68
|
+
return JSON.parse(jsonPayload);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function runCommand(command, args) {
|
|
72
|
+
return await new Promise((resolve, reject)=>{
|
|
73
|
+
const child = (0, external_node_child_process_namespaceObject.spawn)(command, args, {
|
|
74
|
+
stdio: [
|
|
75
|
+
"ignore",
|
|
76
|
+
"pipe",
|
|
77
|
+
"pipe"
|
|
78
|
+
]
|
|
79
|
+
});
|
|
80
|
+
let stdout = "";
|
|
81
|
+
let stderr = "";
|
|
82
|
+
child.stdout.on("data", (chunk)=>{
|
|
83
|
+
stdout += chunk.toString();
|
|
84
|
+
});
|
|
85
|
+
child.stderr.on("data", (chunk)=>{
|
|
86
|
+
stderr += chunk.toString();
|
|
87
|
+
});
|
|
88
|
+
child.on("error", reject);
|
|
89
|
+
child.on("close", (exitCode)=>{
|
|
90
|
+
resolve({
|
|
91
|
+
exitCode,
|
|
92
|
+
stdout,
|
|
93
|
+
stderr
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
async function scanSkillDirectory(skillPath, logger, security) {
|
|
99
|
+
const scanOnInstall = security?.scanOnInstall ?? true;
|
|
100
|
+
if (!scanOnInstall) return;
|
|
101
|
+
const command = getScannerCommand(security);
|
|
102
|
+
(0, uv_cjs_namespaceObject.ensureUvAvailableForFeature)(command, "skills.security.scanOnInstall");
|
|
103
|
+
const args = [
|
|
104
|
+
...getScannerArgs(security),
|
|
105
|
+
skillPath
|
|
106
|
+
];
|
|
107
|
+
logger.info(`Running skill security scan: ${command} ${args.join(" ")}`);
|
|
108
|
+
const result = await runCommand(command, args);
|
|
109
|
+
if (0 !== result.exitCode) {
|
|
110
|
+
const details = result.stderr.trim() || result.stdout.trim();
|
|
111
|
+
throw new Error(`Skill security scan failed with exit code ${result.exitCode ?? "unknown"}${details ? `: ${details}` : ""}`);
|
|
112
|
+
}
|
|
113
|
+
const parsed = parseScanResult(result.stdout);
|
|
114
|
+
const failedPaths = Object.entries(parsed).filter(([, value])=>Boolean(value.error && false !== value.error.is_failure));
|
|
115
|
+
if (failedPaths.length > 0) {
|
|
116
|
+
const formatted = failedPaths.map(([path, value])=>{
|
|
117
|
+
const category = value.error?.category ? ` (${value.error.category})` : "";
|
|
118
|
+
return `${path}: ${value.error?.message || "unknown scan error"}${category}`;
|
|
119
|
+
}).join("; ");
|
|
120
|
+
throw new Error(`Skill security scan reported errors: ${formatted}`);
|
|
121
|
+
}
|
|
122
|
+
const blockedCodes = getBlockedIssueCodes(security);
|
|
123
|
+
const blockingIssues = [];
|
|
124
|
+
const nonBlockingIssues = [];
|
|
125
|
+
for (const value of Object.values(parsed))for (const issue of value.issues || []){
|
|
126
|
+
const code = (issue.code || "").trim().toUpperCase();
|
|
127
|
+
if (!code) continue;
|
|
128
|
+
const issueDetails = {
|
|
129
|
+
code,
|
|
130
|
+
message: issue.message || ""
|
|
131
|
+
};
|
|
132
|
+
if (blockedCodes.has(code)) blockingIssues.push(issueDetails);
|
|
133
|
+
else nonBlockingIssues.push(issueDetails);
|
|
134
|
+
}
|
|
135
|
+
if (nonBlockingIssues.length > 0) {
|
|
136
|
+
const codes = Array.from(new Set(nonBlockingIssues.map((issue)=>issue.code)));
|
|
137
|
+
logger.warn(`Skill security scan returned non-blocking issues: ${codes.join(", ")}`);
|
|
138
|
+
}
|
|
139
|
+
if (blockingIssues.length > 0) {
|
|
140
|
+
const codes = Array.from(new Set(blockingIssues.map((issue)=>issue.code)));
|
|
141
|
+
throw new Error(`Skill security scan blocked installation due to issue codes: ${codes.join(", ")}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const __skillSecurityScanner = {
|
|
145
|
+
parseScanResult,
|
|
146
|
+
getScannerArgs,
|
|
147
|
+
getScannerCommand,
|
|
148
|
+
getBlockedIssueCodes
|
|
149
|
+
};
|
|
150
|
+
exports.__skillSecurityScanner = __webpack_exports__.__skillSecurityScanner;
|
|
151
|
+
exports.scanSkillDirectory = __webpack_exports__.scanSkillDirectory;
|
|
152
|
+
for(var __rspack_i in __webpack_exports__)if (-1 === [
|
|
153
|
+
"__skillSecurityScanner",
|
|
154
|
+
"scanSkillDirectory"
|
|
155
|
+
].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
|
|
156
|
+
Object.defineProperty(exports, '__esModule', {
|
|
157
|
+
value: true
|
|
158
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Logger } from "@/logger.js";
|
|
2
|
+
import type { SkillSecurityOptions } from "../types/skill.js";
|
|
3
|
+
type ScanIssue = {
|
|
4
|
+
code?: string;
|
|
5
|
+
message?: string;
|
|
6
|
+
};
|
|
7
|
+
type ScanError = {
|
|
8
|
+
message?: string;
|
|
9
|
+
is_failure?: boolean;
|
|
10
|
+
category?: string;
|
|
11
|
+
};
|
|
12
|
+
type ScanPathResult = {
|
|
13
|
+
issues?: ScanIssue[];
|
|
14
|
+
error?: ScanError | null;
|
|
15
|
+
};
|
|
16
|
+
type ScanResultMap = Record<string, ScanPathResult>;
|
|
17
|
+
declare function getScannerCommand(security?: SkillSecurityOptions): string;
|
|
18
|
+
declare function getScannerArgs(security?: SkillSecurityOptions): string[];
|
|
19
|
+
declare function getBlockedIssueCodes(security?: SkillSecurityOptions): Set<string>;
|
|
20
|
+
declare function parseScanResult(stdout: string): ScanResultMap;
|
|
21
|
+
export declare function scanSkillDirectory(skillPath: string, logger: Logger, security?: SkillSecurityOptions): Promise<void>;
|
|
22
|
+
export declare const __skillSecurityScanner: {
|
|
23
|
+
parseScanResult: typeof parseScanResult;
|
|
24
|
+
getScannerArgs: typeof getScannerArgs;
|
|
25
|
+
getScannerCommand: typeof getScannerCommand;
|
|
26
|
+
getBlockedIssueCodes: typeof getBlockedIssueCodes;
|
|
27
|
+
};
|
|
28
|
+
export {};
|