@wingman-ai/gateway 0.4.1 → 0.4.2
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/cli/commands/init.cjs +135 -1
- package/dist/cli/commands/init.js +136 -2
- package/dist/cli/commands/skill.cjs +7 -3
- package/dist/cli/commands/skill.js +7 -3
- package/dist/cli/config/loader.cjs +7 -3
- package/dist/cli/config/loader.js +7 -3
- package/dist/cli/config/schema.cjs +27 -9
- package/dist/cli/config/schema.d.ts +18 -4
- package/dist/cli/config/schema.js +23 -8
- package/dist/cli/core/agentInvoker.cjs +49 -11
- package/dist/cli/core/agentInvoker.js +49 -11
- package/dist/cli/services/skillRepository.cjs +155 -69
- package/dist/cli/services/skillRepository.d.ts +7 -2
- package/dist/cli/services/skillRepository.js +155 -69
- package/dist/cli/services/skillService.cjs +93 -26
- package/dist/cli/services/skillService.d.ts +7 -0
- package/dist/cli/services/skillService.js +96 -29
- package/dist/cli/types/skill.d.ts +8 -3
- package/dist/skills/activation.cjs +92 -0
- package/dist/skills/activation.d.ts +12 -0
- package/dist/skills/activation.js +58 -0
- package/dist/skills/bin-requirements.cjs +63 -0
- package/dist/skills/bin-requirements.d.ts +3 -0
- package/dist/skills/bin-requirements.js +26 -0
- package/dist/skills/metadata.cjs +141 -0
- package/dist/skills/metadata.d.ts +29 -0
- package/dist/skills/metadata.js +104 -0
- package/dist/skills/overlay.cjs +75 -0
- package/dist/skills/overlay.d.ts +2 -0
- package/dist/skills/overlay.js +38 -0
- package/dist/tests/cli-config-loader.test.cjs +7 -3
- package/dist/tests/cli-config-loader.test.js +7 -3
- package/dist/tests/cli-init.test.cjs +54 -0
- package/dist/tests/cli-init.test.js +54 -0
- package/dist/tests/config-json-schema.test.cjs +12 -0
- package/dist/tests/config-json-schema.test.js +12 -0
- package/dist/tests/skill-activation.test.cjs +86 -0
- package/dist/tests/skill-activation.test.d.ts +1 -0
- package/dist/tests/skill-activation.test.js +80 -0
- package/dist/tests/skill-metadata.test.cjs +119 -0
- package/dist/tests/skill-metadata.test.d.ts +1 -0
- package/dist/tests/skill-metadata.test.js +113 -0
- package/dist/tests/skill-repository.test.cjs +363 -0
- package/dist/tests/skill-repository.test.js +363 -0
- package/package.json +4 -4
- package/skills/gog/SKILL.md +1 -1
- package/skills/weather/SKILL.md +1 -1
- package/skills/ui-registry/SKILL.md +0 -35
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createLogger } from "../../logger.js";
|
|
2
|
+
import { parseSkillFrontmatter } from "../../skills/metadata.js";
|
|
2
3
|
function _define_property(obj, key, value) {
|
|
3
4
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
4
5
|
value: value,
|
|
@@ -35,7 +36,8 @@ class SkillRepository {
|
|
|
35
36
|
async listAvailableSkills() {
|
|
36
37
|
try {
|
|
37
38
|
if ("clawhub" === this.provider) return await this.listSkillsFromClawhub();
|
|
38
|
-
return await this.listSkillsFromGitHub();
|
|
39
|
+
if ("github" === this.provider) return await this.listSkillsFromGitHub();
|
|
40
|
+
return await this.listSkillsFromHybrid();
|
|
39
41
|
} catch (error) {
|
|
40
42
|
if (error instanceof Error) throw new Error(`Failed to list skills: ${error.message}`);
|
|
41
43
|
throw error;
|
|
@@ -57,19 +59,39 @@ class SkillRepository {
|
|
|
57
59
|
return Buffer.from(arrayBuffer);
|
|
58
60
|
}
|
|
59
61
|
async getSkillMetadata(skillName) {
|
|
60
|
-
if ("clawhub" === this.provider) return await this.getClawhubSkillMetadata(skillName);
|
|
61
62
|
try {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return
|
|
63
|
+
if ("clawhub" === this.provider) return await this.getClawhubSkillMetadata(skillName);
|
|
64
|
+
if ("github" === this.provider) {
|
|
65
|
+
const repository = await this.resolveGitHubRepositoryForSkill(skillName);
|
|
66
|
+
return await this.getGitHubSkillMetadata(skillName, repository);
|
|
67
|
+
}
|
|
68
|
+
return await this.getHybridSkillMetadata(skillName);
|
|
68
69
|
} catch (error) {
|
|
69
70
|
if (error instanceof Error) throw new Error(`Failed to fetch skill metadata for ${skillName}: ${error.message}`);
|
|
70
71
|
throw error;
|
|
71
72
|
}
|
|
72
73
|
}
|
|
74
|
+
async getGitHubSkillMetadata(skillName, repository) {
|
|
75
|
+
const skillMdPath = `/repos/${repository.owner}/${repository.name}/contents/skills/${skillName}/SKILL.md`;
|
|
76
|
+
const skillMd = await this.fetchGitHub(skillMdPath);
|
|
77
|
+
if ("file" !== skillMd.type || !skillMd.content) throw new Error(`SKILL.md not found or invalid in ${repository.owner}/${repository.name}`);
|
|
78
|
+
const content = Buffer.from(skillMd.content, "base64").toString("utf-8");
|
|
79
|
+
return this.parseSkillMetadata(content);
|
|
80
|
+
}
|
|
81
|
+
async resolveGitHubRepositoryForSkill(skillName) {
|
|
82
|
+
const repositories = this.getGitHubRepositories();
|
|
83
|
+
for(let index = repositories.length - 1; index >= 0; index -= 1){
|
|
84
|
+
const repository = repositories[index];
|
|
85
|
+
try {
|
|
86
|
+
await this.fetchGitHub(`/repos/${repository.owner}/${repository.name}/contents/skills/${skillName}/SKILL.md`);
|
|
87
|
+
return repository;
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (error instanceof Error && error.message.includes("Resource not found")) continue;
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
throw new Error(`Skill '${skillName}' not found in configured GitHub repositories: ${repositories.map((repository)=>`${repository.owner}/${repository.name}`).join(", ")}`);
|
|
94
|
+
}
|
|
73
95
|
async getClawhubSkillMetadata(skillName) {
|
|
74
96
|
try {
|
|
75
97
|
const detail = await this.fetchJson(`${this.clawhubBaseUrl}/api/v1/skills/${encodeURIComponent(skillName)}`, {
|
|
@@ -99,51 +121,50 @@ class SkillRepository {
|
|
|
99
121
|
throw error;
|
|
100
122
|
}
|
|
101
123
|
}
|
|
124
|
+
async getHybridSkillMetadata(skillName) {
|
|
125
|
+
let githubError;
|
|
126
|
+
try {
|
|
127
|
+
const repository = await this.resolveGitHubRepositoryForSkill(skillName);
|
|
128
|
+
return await this.getGitHubSkillMetadata(skillName, repository);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
githubError = error;
|
|
131
|
+
logger.debug(`Falling back to ClawHub metadata lookup for '${skillName}' after GitHub error: ${error instanceof Error ? error.message : String(error)}`);
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
return await this.getClawhubSkillMetadata(skillName);
|
|
135
|
+
} catch (clawhubError) {
|
|
136
|
+
throw new Error(`GitHub error: ${githubError instanceof Error ? githubError.message : String(githubError)}; ClawHub error: ${clawhubError instanceof Error ? clawhubError.message : String(clawhubError)}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
102
139
|
parseSkillMetadata(content) {
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
140
|
+
const parsed = parseSkillFrontmatter(content);
|
|
141
|
+
return {
|
|
142
|
+
name: parsed.name,
|
|
143
|
+
description: parsed.description,
|
|
144
|
+
...parsed.license ? {
|
|
145
|
+
license: parsed.license
|
|
146
|
+
} : {},
|
|
147
|
+
...parsed.compatibility ? {
|
|
148
|
+
compatibility: parsed.compatibility
|
|
149
|
+
} : {},
|
|
150
|
+
...parsed.allowedTools.length > 0 ? {
|
|
151
|
+
allowedTools: parsed.allowedTools
|
|
152
|
+
} : {},
|
|
153
|
+
...parsed.metadata ? {
|
|
154
|
+
metadata: parsed.metadata
|
|
155
|
+
} : {}
|
|
110
156
|
};
|
|
111
|
-
const lines = frontmatter.split("\n");
|
|
112
|
-
for (const line of lines){
|
|
113
|
-
const colonIndex = line.indexOf(":");
|
|
114
|
-
if (-1 === colonIndex) continue;
|
|
115
|
-
const key = line.substring(0, colonIndex).trim();
|
|
116
|
-
const value = line.substring(colonIndex + 1).trim();
|
|
117
|
-
switch(key){
|
|
118
|
-
case "name":
|
|
119
|
-
metadata.name = value;
|
|
120
|
-
break;
|
|
121
|
-
case "description":
|
|
122
|
-
metadata.description = value;
|
|
123
|
-
break;
|
|
124
|
-
case "license":
|
|
125
|
-
metadata.license = value;
|
|
126
|
-
break;
|
|
127
|
-
case "compatibility":
|
|
128
|
-
metadata.compatibility = value;
|
|
129
|
-
break;
|
|
130
|
-
case "allowed-tools":
|
|
131
|
-
metadata.allowedTools = value;
|
|
132
|
-
break;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
if (!metadata.name) throw new Error("Invalid SKILL.md: missing required field 'name'");
|
|
136
|
-
if (!metadata.description) throw new Error("Invalid SKILL.md: missing required field 'description'");
|
|
137
|
-
const nameRegex = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
138
|
-
if (!nameRegex.test(metadata.name)) throw new Error(`Invalid skill name '${metadata.name}': must be lowercase alphanumeric with hyphens only`);
|
|
139
|
-
return metadata;
|
|
140
157
|
}
|
|
141
158
|
async downloadSkill(skillName) {
|
|
142
|
-
if ("clawhub" === this.provider) return await this.downloadSkillFromClawhub(skillName);
|
|
143
159
|
try {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
160
|
+
if ("clawhub" === this.provider) return await this.downloadSkillFromClawhub(skillName);
|
|
161
|
+
if ("github" === this.provider) {
|
|
162
|
+
const repository = await this.resolveGitHubRepositoryForSkill(skillName);
|
|
163
|
+
const files = new Map();
|
|
164
|
+
await this.downloadDirectory(`skills/${skillName}`, files, skillName, repository);
|
|
165
|
+
return files;
|
|
166
|
+
}
|
|
167
|
+
return await this.downloadHybridSkill(skillName);
|
|
147
168
|
} catch (error) {
|
|
148
169
|
if (error instanceof Error) throw new Error(`Failed to download skill ${skillName}: ${error.message}`);
|
|
149
170
|
throw error;
|
|
@@ -186,8 +207,25 @@ class SkillRepository {
|
|
|
186
207
|
throw error;
|
|
187
208
|
}
|
|
188
209
|
}
|
|
189
|
-
async
|
|
190
|
-
|
|
210
|
+
async downloadHybridSkill(skillName) {
|
|
211
|
+
let githubError;
|
|
212
|
+
try {
|
|
213
|
+
const repository = await this.resolveGitHubRepositoryForSkill(skillName);
|
|
214
|
+
const files = new Map();
|
|
215
|
+
await this.downloadDirectory(`skills/${skillName}`, files, skillName, repository);
|
|
216
|
+
return files;
|
|
217
|
+
} catch (error) {
|
|
218
|
+
githubError = error;
|
|
219
|
+
logger.debug(`Falling back to ClawHub download for '${skillName}' after GitHub error: ${error instanceof Error ? error.message : String(error)}`);
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
return await this.downloadSkillFromClawhub(skillName);
|
|
223
|
+
} catch (clawhubError) {
|
|
224
|
+
throw new Error(`GitHub error: ${githubError instanceof Error ? githubError.message : String(githubError)}; ClawHub error: ${clawhubError instanceof Error ? clawhubError.message : String(clawhubError)}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async downloadDirectory(path, files, skillName, repository) {
|
|
228
|
+
const contents = await this.fetchGitHub(`/repos/${repository.owner}/${repository.name}/contents/${path}`);
|
|
191
229
|
for (const item of contents)if ("file" === item.type) if (item.content) {
|
|
192
230
|
const content = Buffer.from(item.content, "base64");
|
|
193
231
|
const relativePath = item.path.replace(`skills/${skillName}/`, "");
|
|
@@ -200,7 +238,7 @@ class SkillRepository {
|
|
|
200
238
|
files.set(relativePath, content);
|
|
201
239
|
}
|
|
202
240
|
}
|
|
203
|
-
else if ("dir" === item.type) await this.downloadDirectory(item.path, files, skillName);
|
|
241
|
+
else if ("dir" === item.type) await this.downloadDirectory(item.path, files, skillName, repository);
|
|
204
242
|
}
|
|
205
243
|
async listSkillsFromClawhub() {
|
|
206
244
|
const allSkills = [];
|
|
@@ -229,32 +267,80 @@ class SkillRepository {
|
|
|
229
267
|
}while (cursor);
|
|
230
268
|
return allSkills;
|
|
231
269
|
}
|
|
232
|
-
async
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
description: metadata.description || "No description",
|
|
240
|
-
path: item.path,
|
|
241
|
-
metadata
|
|
242
|
-
});
|
|
270
|
+
async listSkillsFromHybrid() {
|
|
271
|
+
let clawhubSkills = [];
|
|
272
|
+
let githubSkills = [];
|
|
273
|
+
let clawhubError;
|
|
274
|
+
let githubError;
|
|
275
|
+
try {
|
|
276
|
+
clawhubSkills = await this.listSkillsFromClawhub();
|
|
243
277
|
} catch (error) {
|
|
244
|
-
|
|
278
|
+
clawhubError = error;
|
|
279
|
+
logger.warn(`Failed to list ClawHub skills in hybrid mode: ${error instanceof Error ? error.message : String(error)}`);
|
|
245
280
|
}
|
|
246
|
-
|
|
281
|
+
try {
|
|
282
|
+
githubSkills = await this.listSkillsFromGitHub();
|
|
283
|
+
} catch (error) {
|
|
284
|
+
githubError = error;
|
|
285
|
+
logger.warn(`Failed to list GitHub skills in hybrid mode: ${error instanceof Error ? error.message : String(error)}`);
|
|
286
|
+
}
|
|
287
|
+
if (0 === clawhubSkills.length && 0 === githubSkills.length) throw new Error(`No skill sources available. ClawHub error: ${clawhubError instanceof Error ? clawhubError.message : "none"}; GitHub error: ${githubError instanceof Error ? githubError.message : "none"}`);
|
|
288
|
+
const mergedSkills = new Map();
|
|
289
|
+
for (const skill of clawhubSkills)mergedSkills.set(skill.name, skill);
|
|
290
|
+
for (const skill of githubSkills){
|
|
291
|
+
if (mergedSkills.has(skill.name)) mergedSkills.delete(skill.name);
|
|
292
|
+
mergedSkills.set(skill.name, skill);
|
|
293
|
+
}
|
|
294
|
+
return [
|
|
295
|
+
...mergedSkills.values()
|
|
296
|
+
];
|
|
297
|
+
}
|
|
298
|
+
async listSkillsFromGitHub() {
|
|
299
|
+
const mergedSkills = new Map();
|
|
300
|
+
for (const repository of this.getGitHubRepositories()){
|
|
301
|
+
const contents = await this.fetchGitHub(`/repos/${repository.owner}/${repository.name}/contents/skills`);
|
|
302
|
+
for (const item of contents)if ("dir" === item.type) try {
|
|
303
|
+
const metadata = await this.getGitHubSkillMetadata(item.name, repository);
|
|
304
|
+
if (mergedSkills.has(item.name)) mergedSkills.delete(item.name);
|
|
305
|
+
mergedSkills.set(item.name, {
|
|
306
|
+
name: item.name,
|
|
307
|
+
description: metadata.description || "No description",
|
|
308
|
+
path: item.path,
|
|
309
|
+
metadata
|
|
310
|
+
});
|
|
311
|
+
} catch (error) {
|
|
312
|
+
logger.warn(`Could not read skill ${item.name} from ${repository.owner}/${repository.name}`, error instanceof Error ? error.message : String(error));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return [
|
|
316
|
+
...mergedSkills.values()
|
|
317
|
+
];
|
|
318
|
+
}
|
|
319
|
+
getGitHubRepositories() {
|
|
320
|
+
if (this.repositories.length > 0) return this.repositories;
|
|
321
|
+
throw new Error("No GitHub skill repositories configured. Set skills.repositories or the legacy skills.repositoryOwner + skills.repositoryName fields.");
|
|
247
322
|
}
|
|
248
323
|
constructor(options = {}){
|
|
249
324
|
_define_property(this, "githubBaseUrl", "https://api.github.com");
|
|
250
|
-
_define_property(this, "
|
|
251
|
-
_define_property(this, "repo", void 0);
|
|
325
|
+
_define_property(this, "repositories", void 0);
|
|
252
326
|
_define_property(this, "token", void 0);
|
|
253
327
|
_define_property(this, "provider", void 0);
|
|
254
328
|
_define_property(this, "clawhubBaseUrl", void 0);
|
|
255
|
-
this.provider = options.provider || "
|
|
256
|
-
|
|
257
|
-
|
|
329
|
+
this.provider = options.provider || "hybrid";
|
|
330
|
+
const normalizedRepositories = (options.repositories || []).map((repository)=>({
|
|
331
|
+
owner: repository.owner.trim(),
|
|
332
|
+
name: repository.name.trim()
|
|
333
|
+
})).filter((repository)=>repository.owner && repository.name);
|
|
334
|
+
const legacyOwner = options.repositoryOwner?.trim();
|
|
335
|
+
const legacyName = options.repositoryName?.trim();
|
|
336
|
+
if (normalizedRepositories.length > 0) this.repositories = normalizedRepositories;
|
|
337
|
+
else if (legacyOwner && legacyName) this.repositories = [
|
|
338
|
+
{
|
|
339
|
+
owner: legacyOwner,
|
|
340
|
+
name: legacyName
|
|
341
|
+
}
|
|
342
|
+
];
|
|
343
|
+
else this.repositories = [];
|
|
258
344
|
this.token = options.githubToken || process.env.GITHUB_TOKEN || void 0;
|
|
259
345
|
this.clawhubBaseUrl = (options.clawhubBaseUrl || "https://clawhub.ai").replace(/\/+$/, "");
|
|
260
346
|
}
|
|
@@ -26,12 +26,15 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
26
26
|
__webpack_require__.d(__webpack_exports__, {
|
|
27
27
|
SkillService: ()=>SkillService
|
|
28
28
|
});
|
|
29
|
+
const external_node_child_process_namespaceObject = require("node:child_process");
|
|
29
30
|
const promises_namespaceObject = require("node:fs/promises");
|
|
30
31
|
const external_node_os_namespaceObject = require("node:os");
|
|
31
32
|
const external_node_path_namespaceObject = require("node:path");
|
|
32
33
|
const external_node_readline_promises_namespaceObject = require("node:readline/promises");
|
|
33
34
|
const external_logger_cjs_namespaceObject = require("../../logger.cjs");
|
|
34
35
|
const external_skillSecurityScanner_cjs_namespaceObject = require("./skillSecurityScanner.cjs");
|
|
36
|
+
const bin_requirements_cjs_namespaceObject = require("../../skills/bin-requirements.cjs");
|
|
37
|
+
const metadata_cjs_namespaceObject = require("../../skills/metadata.cjs");
|
|
35
38
|
function _define_property(obj, key, value) {
|
|
36
39
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
37
40
|
value: value,
|
|
@@ -146,13 +149,24 @@ class SkillService {
|
|
|
146
149
|
recursive: true,
|
|
147
150
|
force: true
|
|
148
151
|
});
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
152
|
+
const dependencyStatus = await this.handlePostInstallDependencyActivation(skillPath);
|
|
153
|
+
if ("interactive" === this.outputManager.getMode()) {
|
|
154
|
+
console.log(`\n✓ Successfully installed skill ${skillName} to ${skillPath}`);
|
|
155
|
+
if (dependencyStatus) console.log(`\n${dependencyStatus}`);
|
|
156
|
+
} else {
|
|
157
|
+
this.outputManager.emitEvent({
|
|
158
|
+
type: "skill-install-complete",
|
|
159
|
+
skill: skillName,
|
|
160
|
+
path: skillPath,
|
|
161
|
+
timestamp: new Date().toISOString()
|
|
162
|
+
});
|
|
163
|
+
if (dependencyStatus) this.outputManager.emitEvent({
|
|
164
|
+
type: "log",
|
|
165
|
+
level: "info",
|
|
166
|
+
message: dependencyStatus,
|
|
167
|
+
timestamp: new Date().toISOString()
|
|
168
|
+
});
|
|
169
|
+
}
|
|
156
170
|
} catch (error) {
|
|
157
171
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
158
172
|
const logFile = (0, external_logger_cjs_namespaceObject.getLogFilePath)();
|
|
@@ -304,6 +318,75 @@ class SkillService {
|
|
|
304
318
|
rl.close();
|
|
305
319
|
}
|
|
306
320
|
}
|
|
321
|
+
async loadSkillFrontmatter(skillPath) {
|
|
322
|
+
const skillMdPath = external_node_path_namespaceObject.join(skillPath, "SKILL.md");
|
|
323
|
+
const content = await promises_namespaceObject.readFile(skillMdPath, "utf-8");
|
|
324
|
+
return (0, metadata_cjs_namespaceObject.parseSkillFrontmatter)(content);
|
|
325
|
+
}
|
|
326
|
+
resolveMissingRequiredBins(skill) {
|
|
327
|
+
const requiredBins = skill.runtimeMetadata?.requires.bins || [];
|
|
328
|
+
return (0, bin_requirements_cjs_namespaceObject.findMissingBins)(requiredBins);
|
|
329
|
+
}
|
|
330
|
+
selectInstallRecipe(skill, missingBins) {
|
|
331
|
+
const installRecipes = skill.runtimeMetadata?.install || [];
|
|
332
|
+
if (0 === installRecipes.length) return null;
|
|
333
|
+
const missing = new Set(missingBins);
|
|
334
|
+
const withMatchingBins = installRecipes.find((recipe)=>recipe.bins.some((bin)=>missing.has(bin)));
|
|
335
|
+
return withMatchingBins || installRecipes[0] || null;
|
|
336
|
+
}
|
|
337
|
+
async promptForDependencyInstall(skillName, missingBins, recipe) {
|
|
338
|
+
const commandPreview = this.getInstallCommandPreview(recipe);
|
|
339
|
+
if (!commandPreview) return false;
|
|
340
|
+
console.log(`Skill '${skillName}' is installed but inactive. Missing required binaries: ${missingBins.join(", ")}`);
|
|
341
|
+
console.log(`Install option (${recipe.kind}): ${commandPreview}`);
|
|
342
|
+
const rl = external_node_readline_promises_namespaceObject.createInterface({
|
|
343
|
+
input: process.stdin,
|
|
344
|
+
output: process.stdout
|
|
345
|
+
});
|
|
346
|
+
try {
|
|
347
|
+
const answer = await rl.question("Run install command now? (y/N): ");
|
|
348
|
+
const normalized = answer.trim().toLowerCase();
|
|
349
|
+
return "y" === normalized || "yes" === normalized;
|
|
350
|
+
} finally{
|
|
351
|
+
rl.close();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
getInstallCommandPreview(recipe) {
|
|
355
|
+
if ("brew" === recipe.kind && recipe.formula) return `brew install ${recipe.formula}`;
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
async runInstallRecipe(recipe) {
|
|
359
|
+
if ("brew" !== recipe.kind || !recipe.formula) throw new Error(`Unsupported install recipe kind '${recipe.kind}'. Currently supported: brew`);
|
|
360
|
+
await new Promise((resolve, reject)=>{
|
|
361
|
+
const child = (0, external_node_child_process_namespaceObject.spawn)("brew", [
|
|
362
|
+
"install",
|
|
363
|
+
recipe.formula
|
|
364
|
+
], {
|
|
365
|
+
cwd: this.workspace,
|
|
366
|
+
stdio: "inherit"
|
|
367
|
+
});
|
|
368
|
+
child.on("error", (error)=>reject(error));
|
|
369
|
+
child.on("exit", (code)=>{
|
|
370
|
+
if (0 === code) return void resolve();
|
|
371
|
+
reject(new Error(`Install command exited with code ${code}`));
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
async handlePostInstallDependencyActivation(skillPath) {
|
|
376
|
+
const skill = await this.loadSkillFrontmatter(skillPath);
|
|
377
|
+
if (!skill.runtimeMetadata) return null;
|
|
378
|
+
const missingBins = this.resolveMissingRequiredBins(skill);
|
|
379
|
+
if (0 === missingBins.length) return `Skill '${skill.name}' is active.`;
|
|
380
|
+
const installRecipe = this.selectInstallRecipe(skill, missingBins);
|
|
381
|
+
const outputMode = this.outputManager.getMode();
|
|
382
|
+
if (!installRecipe || "interactive" !== outputMode) return `Skill '${skill.name}' remains inactive until required binaries are available: ${missingBins.join(", ")}`;
|
|
383
|
+
const confirmed = await this.promptForDependencyInstall(skill.name, missingBins, installRecipe);
|
|
384
|
+
if (!confirmed) return `Skill '${skill.name}' remains inactive until required binaries are available: ${missingBins.join(", ")}`;
|
|
385
|
+
await this.runInstallRecipe(installRecipe);
|
|
386
|
+
const remainingMissingBins = this.resolveMissingRequiredBins(skill);
|
|
387
|
+
if (0 === remainingMissingBins.length) return `Skill '${skill.name}' is now active.`;
|
|
388
|
+
return `Skill '${skill.name}' is still inactive. Missing binaries: ${remainingMissingBins.join(", ")}`;
|
|
389
|
+
}
|
|
307
390
|
async validateSkillMd(skillPath) {
|
|
308
391
|
const skillMdPath = external_node_path_namespaceObject.join(skillPath, "SKILL.md");
|
|
309
392
|
try {
|
|
@@ -322,26 +405,10 @@ class SkillService {
|
|
|
322
405
|
return filePath;
|
|
323
406
|
}
|
|
324
407
|
parseSkillMetadata(content) {
|
|
325
|
-
const
|
|
326
|
-
const match = content.match(frontmatterRegex);
|
|
327
|
-
if (!match) throw new Error("Invalid SKILL.md format: missing YAML frontmatter");
|
|
328
|
-
const frontmatter = match[1];
|
|
329
|
-
let name = "";
|
|
330
|
-
let description = "";
|
|
331
|
-
const lines = frontmatter.split("\n");
|
|
332
|
-
for (const line of lines){
|
|
333
|
-
const colonIndex = line.indexOf(":");
|
|
334
|
-
if (-1 === colonIndex) continue;
|
|
335
|
-
const key = line.substring(0, colonIndex).trim();
|
|
336
|
-
const value = line.substring(colonIndex + 1).trim();
|
|
337
|
-
if ("name" === key) name = value;
|
|
338
|
-
else if ("description" === key) description = value;
|
|
339
|
-
}
|
|
340
|
-
if (!name) throw new Error("missing required field 'name'");
|
|
341
|
-
if (!description) throw new Error("missing required field 'description'");
|
|
408
|
+
const parsed = (0, metadata_cjs_namespaceObject.parseSkillFrontmatter)(content);
|
|
342
409
|
return {
|
|
343
|
-
name,
|
|
344
|
-
description
|
|
410
|
+
name: parsed.name,
|
|
411
|
+
description: parsed.description
|
|
345
412
|
};
|
|
346
413
|
}
|
|
347
414
|
constructor(repository, outputManager, logger, options){
|
|
@@ -38,6 +38,13 @@ export declare class SkillService {
|
|
|
38
38
|
* Prompt user for overwrite confirmation
|
|
39
39
|
*/
|
|
40
40
|
private promptForOverwrite;
|
|
41
|
+
private loadSkillFrontmatter;
|
|
42
|
+
private resolveMissingRequiredBins;
|
|
43
|
+
private selectInstallRecipe;
|
|
44
|
+
private promptForDependencyInstall;
|
|
45
|
+
private getInstallCommandPreview;
|
|
46
|
+
private runInstallRecipe;
|
|
47
|
+
private handlePostInstallDependencyActivation;
|
|
41
48
|
/**
|
|
42
49
|
* Validate SKILL.md file
|
|
43
50
|
*/
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
1
2
|
import { access, cp, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
2
3
|
import { tmpdir } from "node:os";
|
|
3
|
-
import { dirname, join, posix, resolve, sep } from "node:path";
|
|
4
|
+
import { dirname, join, posix, resolve as external_node_path_resolve, sep } from "node:path";
|
|
4
5
|
import { createInterface } from "node:readline/promises";
|
|
5
6
|
import { getLogFilePath } from "../../logger.js";
|
|
6
7
|
import { scanSkillDirectory } from "./skillSecurityScanner.js";
|
|
8
|
+
import { findMissingBins } from "../../skills/bin-requirements.js";
|
|
9
|
+
import { parseSkillFrontmatter } from "../../skills/metadata.js";
|
|
7
10
|
function _define_property(obj, key, value) {
|
|
8
11
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
9
12
|
value: value,
|
|
@@ -118,13 +121,24 @@ class SkillService {
|
|
|
118
121
|
recursive: true,
|
|
119
122
|
force: true
|
|
120
123
|
});
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
124
|
+
const dependencyStatus = await this.handlePostInstallDependencyActivation(skillPath);
|
|
125
|
+
if ("interactive" === this.outputManager.getMode()) {
|
|
126
|
+
console.log(`\n✓ Successfully installed skill ${skillName} to ${skillPath}`);
|
|
127
|
+
if (dependencyStatus) console.log(`\n${dependencyStatus}`);
|
|
128
|
+
} else {
|
|
129
|
+
this.outputManager.emitEvent({
|
|
130
|
+
type: "skill-install-complete",
|
|
131
|
+
skill: skillName,
|
|
132
|
+
path: skillPath,
|
|
133
|
+
timestamp: new Date().toISOString()
|
|
134
|
+
});
|
|
135
|
+
if (dependencyStatus) this.outputManager.emitEvent({
|
|
136
|
+
type: "log",
|
|
137
|
+
level: "info",
|
|
138
|
+
message: dependencyStatus,
|
|
139
|
+
timestamp: new Date().toISOString()
|
|
140
|
+
});
|
|
141
|
+
}
|
|
128
142
|
} catch (error) {
|
|
129
143
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
130
144
|
const logFile = getLogFilePath();
|
|
@@ -276,6 +290,75 @@ class SkillService {
|
|
|
276
290
|
rl.close();
|
|
277
291
|
}
|
|
278
292
|
}
|
|
293
|
+
async loadSkillFrontmatter(skillPath) {
|
|
294
|
+
const skillMdPath = join(skillPath, "SKILL.md");
|
|
295
|
+
const content = await readFile(skillMdPath, "utf-8");
|
|
296
|
+
return parseSkillFrontmatter(content);
|
|
297
|
+
}
|
|
298
|
+
resolveMissingRequiredBins(skill) {
|
|
299
|
+
const requiredBins = skill.runtimeMetadata?.requires.bins || [];
|
|
300
|
+
return findMissingBins(requiredBins);
|
|
301
|
+
}
|
|
302
|
+
selectInstallRecipe(skill, missingBins) {
|
|
303
|
+
const installRecipes = skill.runtimeMetadata?.install || [];
|
|
304
|
+
if (0 === installRecipes.length) return null;
|
|
305
|
+
const missing = new Set(missingBins);
|
|
306
|
+
const withMatchingBins = installRecipes.find((recipe)=>recipe.bins.some((bin)=>missing.has(bin)));
|
|
307
|
+
return withMatchingBins || installRecipes[0] || null;
|
|
308
|
+
}
|
|
309
|
+
async promptForDependencyInstall(skillName, missingBins, recipe) {
|
|
310
|
+
const commandPreview = this.getInstallCommandPreview(recipe);
|
|
311
|
+
if (!commandPreview) return false;
|
|
312
|
+
console.log(`Skill '${skillName}' is installed but inactive. Missing required binaries: ${missingBins.join(", ")}`);
|
|
313
|
+
console.log(`Install option (${recipe.kind}): ${commandPreview}`);
|
|
314
|
+
const rl = createInterface({
|
|
315
|
+
input: process.stdin,
|
|
316
|
+
output: process.stdout
|
|
317
|
+
});
|
|
318
|
+
try {
|
|
319
|
+
const answer = await rl.question("Run install command now? (y/N): ");
|
|
320
|
+
const normalized = answer.trim().toLowerCase();
|
|
321
|
+
return "y" === normalized || "yes" === normalized;
|
|
322
|
+
} finally{
|
|
323
|
+
rl.close();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
getInstallCommandPreview(recipe) {
|
|
327
|
+
if ("brew" === recipe.kind && recipe.formula) return `brew install ${recipe.formula}`;
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
async runInstallRecipe(recipe) {
|
|
331
|
+
if ("brew" !== recipe.kind || !recipe.formula) throw new Error(`Unsupported install recipe kind '${recipe.kind}'. Currently supported: brew`);
|
|
332
|
+
await new Promise((resolve, reject)=>{
|
|
333
|
+
const child = spawn("brew", [
|
|
334
|
+
"install",
|
|
335
|
+
recipe.formula
|
|
336
|
+
], {
|
|
337
|
+
cwd: this.workspace,
|
|
338
|
+
stdio: "inherit"
|
|
339
|
+
});
|
|
340
|
+
child.on("error", (error)=>reject(error));
|
|
341
|
+
child.on("exit", (code)=>{
|
|
342
|
+
if (0 === code) return void resolve();
|
|
343
|
+
reject(new Error(`Install command exited with code ${code}`));
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
async handlePostInstallDependencyActivation(skillPath) {
|
|
348
|
+
const skill = await this.loadSkillFrontmatter(skillPath);
|
|
349
|
+
if (!skill.runtimeMetadata) return null;
|
|
350
|
+
const missingBins = this.resolveMissingRequiredBins(skill);
|
|
351
|
+
if (0 === missingBins.length) return `Skill '${skill.name}' is active.`;
|
|
352
|
+
const installRecipe = this.selectInstallRecipe(skill, missingBins);
|
|
353
|
+
const outputMode = this.outputManager.getMode();
|
|
354
|
+
if (!installRecipe || "interactive" !== outputMode) return `Skill '${skill.name}' remains inactive until required binaries are available: ${missingBins.join(", ")}`;
|
|
355
|
+
const confirmed = await this.promptForDependencyInstall(skill.name, missingBins, installRecipe);
|
|
356
|
+
if (!confirmed) return `Skill '${skill.name}' remains inactive until required binaries are available: ${missingBins.join(", ")}`;
|
|
357
|
+
await this.runInstallRecipe(installRecipe);
|
|
358
|
+
const remainingMissingBins = this.resolveMissingRequiredBins(skill);
|
|
359
|
+
if (0 === remainingMissingBins.length) return `Skill '${skill.name}' is now active.`;
|
|
360
|
+
return `Skill '${skill.name}' is still inactive. Missing binaries: ${remainingMissingBins.join(", ")}`;
|
|
361
|
+
}
|
|
279
362
|
async validateSkillMd(skillPath) {
|
|
280
363
|
const skillMdPath = join(skillPath, "SKILL.md");
|
|
281
364
|
try {
|
|
@@ -288,32 +371,16 @@ class SkillService {
|
|
|
288
371
|
resolveSafeInstallPath(root, relativePath) {
|
|
289
372
|
const normalized = posix.normalize(relativePath.replace(/\\/g, "/")).replace(/^\/+/, "");
|
|
290
373
|
if (!normalized || "." === normalized || normalized.startsWith("../")) throw new Error(`Unsafe skill file path '${relativePath}' rejected during installation`);
|
|
291
|
-
const rootResolved =
|
|
292
|
-
const filePath =
|
|
374
|
+
const rootResolved = external_node_path_resolve(root);
|
|
375
|
+
const filePath = external_node_path_resolve(rootResolved, normalized);
|
|
293
376
|
if (filePath !== rootResolved && !filePath.startsWith(rootResolved + sep)) throw new Error(`Unsafe skill file path '${relativePath}' rejected during installation`);
|
|
294
377
|
return filePath;
|
|
295
378
|
}
|
|
296
379
|
parseSkillMetadata(content) {
|
|
297
|
-
const
|
|
298
|
-
const match = content.match(frontmatterRegex);
|
|
299
|
-
if (!match) throw new Error("Invalid SKILL.md format: missing YAML frontmatter");
|
|
300
|
-
const frontmatter = match[1];
|
|
301
|
-
let name = "";
|
|
302
|
-
let description = "";
|
|
303
|
-
const lines = frontmatter.split("\n");
|
|
304
|
-
for (const line of lines){
|
|
305
|
-
const colonIndex = line.indexOf(":");
|
|
306
|
-
if (-1 === colonIndex) continue;
|
|
307
|
-
const key = line.substring(0, colonIndex).trim();
|
|
308
|
-
const value = line.substring(colonIndex + 1).trim();
|
|
309
|
-
if ("name" === key) name = value;
|
|
310
|
-
else if ("description" === key) description = value;
|
|
311
|
-
}
|
|
312
|
-
if (!name) throw new Error("missing required field 'name'");
|
|
313
|
-
if (!description) throw new Error("missing required field 'description'");
|
|
380
|
+
const parsed = parseSkillFrontmatter(content);
|
|
314
381
|
return {
|
|
315
|
-
name,
|
|
316
|
-
description
|
|
382
|
+
name: parsed.name,
|
|
383
|
+
description: parsed.description
|
|
317
384
|
};
|
|
318
385
|
}
|
|
319
386
|
constructor(repository, outputManager, logger, options){
|
|
@@ -8,8 +8,8 @@ export interface SkillMetadata {
|
|
|
8
8
|
description: string;
|
|
9
9
|
license?: string;
|
|
10
10
|
compatibility?: string;
|
|
11
|
-
metadata?: Record<string,
|
|
12
|
-
allowedTools?: string;
|
|
11
|
+
metadata?: Record<string, unknown>;
|
|
12
|
+
allowedTools?: string[];
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
15
|
* Skill information with additional context
|
|
@@ -57,12 +57,17 @@ export interface SkillCommandArgs {
|
|
|
57
57
|
* Options for skill repository operations
|
|
58
58
|
*/
|
|
59
59
|
export interface SkillRepositoryOptions {
|
|
60
|
-
provider?: "github" | "clawhub";
|
|
60
|
+
provider?: "github" | "clawhub" | "hybrid";
|
|
61
61
|
repositoryOwner?: string;
|
|
62
62
|
repositoryName?: string;
|
|
63
|
+
repositories?: SkillGitHubRepository[];
|
|
63
64
|
githubToken?: string;
|
|
64
65
|
clawhubBaseUrl?: string;
|
|
65
66
|
}
|
|
67
|
+
export interface SkillGitHubRepository {
|
|
68
|
+
owner: string;
|
|
69
|
+
name: string;
|
|
70
|
+
}
|
|
66
71
|
export interface SkillSecurityOptions {
|
|
67
72
|
scanOnInstall?: boolean;
|
|
68
73
|
scannerCommand?: string;
|