@wingman-ai/gateway 0.3.1 → 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/internet_search.test.cjs +22 -28
- package/dist/agent/tests/internet_search.test.js +22 -28
- package/dist/agent/tests/mcpClientManager.test.cjs +50 -0
- package/dist/agent/tests/mcpClientManager.test.js +50 -0
- package/dist/agent/tools/internet_search.cjs +9 -5
- package/dist/agent/tools/internet_search.js +9 -5
- 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/dist/webui/assets/index-Cwkg4DKj.css +11 -0
- package/dist/webui/assets/{index-C8-oboEC.js → index-DHbfLOUR.js} +21 -19
- package/dist/webui/index.html +2 -2
- package/package.json +2 -3
- package/skills/gog/SKILL.md +36 -0
- package/skills/weather/SKILL.md +49 -0
- package/dist/webui/assets/index-BW9nM0J2.css +0 -11
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { WingmanAgentConfig } from "@/agent/config/agentConfig.js";
|
|
2
|
+
import { type MCPProxyConfig } from "@/agent/config/mcpClientManager.js";
|
|
2
3
|
import { type TerminalSessionManager } from "@/agent/tools/terminal_session_manager.js";
|
|
3
4
|
import type { WingmanAgent } from "@/types/agents.js";
|
|
4
5
|
import type { Logger } from "../../logger.js";
|
|
@@ -14,6 +15,7 @@ export interface AgentInvokerOptions {
|
|
|
14
15
|
terminalSessionManager?: TerminalSessionManager;
|
|
15
16
|
workdir?: string | null;
|
|
16
17
|
defaultOutputDir?: string | null;
|
|
18
|
+
mcpProxyConfig?: MCPProxyConfig;
|
|
17
19
|
}
|
|
18
20
|
export interface InvokeAgentOptions {
|
|
19
21
|
signal?: AbortSignal;
|
|
@@ -149,6 +151,7 @@ export declare class AgentInvoker {
|
|
|
149
151
|
private terminalSessionManager;
|
|
150
152
|
private workdir;
|
|
151
153
|
private defaultOutputDir;
|
|
154
|
+
private mcpProxyConfig;
|
|
152
155
|
constructor(options: AgentInvokerOptions);
|
|
153
156
|
findAllAgents(): WingmanAgentConfig[];
|
|
154
157
|
/**
|
|
@@ -296,7 +296,8 @@ class AgentInvoker {
|
|
|
296
296
|
if (mcpConfigs.length > 0) {
|
|
297
297
|
this.logger.debug("Initializing MCP client for agent invocation");
|
|
298
298
|
this.mcpManager = new MCPClientManager(mcpConfigs, this.logger, {
|
|
299
|
-
executionWorkspace
|
|
299
|
+
executionWorkspace,
|
|
300
|
+
proxyConfig: this.mcpProxyConfig
|
|
300
301
|
});
|
|
301
302
|
await this.mcpManager.initialize();
|
|
302
303
|
const mcpTools = await this.mcpManager.getTools();
|
|
@@ -551,6 +552,7 @@ class AgentInvoker {
|
|
|
551
552
|
_define_property(this, "terminalSessionManager", void 0);
|
|
552
553
|
_define_property(this, "workdir", null);
|
|
553
554
|
_define_property(this, "defaultOutputDir", null);
|
|
555
|
+
_define_property(this, "mcpProxyConfig", void 0);
|
|
554
556
|
this.outputManager = options.outputManager;
|
|
555
557
|
this.logger = options.logger;
|
|
556
558
|
this.workspace = options.workspace || process.cwd();
|
|
@@ -559,6 +561,7 @@ class AgentInvoker {
|
|
|
559
561
|
this.terminalSessionManager = options.terminalSessionManager || getSharedTerminalSessionManager();
|
|
560
562
|
this.workdir = options.workdir || null;
|
|
561
563
|
this.defaultOutputDir = options.defaultOutputDir || null;
|
|
564
|
+
this.mcpProxyConfig = options.mcpProxyConfig;
|
|
562
565
|
const configLoader = new WingmanConfigLoader(this.configDir, this.workspace);
|
|
563
566
|
this.wingmanConfig = configLoader.loadConfig();
|
|
564
567
|
this.loader = new AgentLoader(this.configDir, this.workspace, this.wingmanConfig);
|
|
@@ -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 };
|