@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.
Files changed (48) hide show
  1. package/dist/cli/commands/init.cjs +135 -1
  2. package/dist/cli/commands/init.js +136 -2
  3. package/dist/cli/commands/skill.cjs +7 -3
  4. package/dist/cli/commands/skill.js +7 -3
  5. package/dist/cli/config/loader.cjs +7 -3
  6. package/dist/cli/config/loader.js +7 -3
  7. package/dist/cli/config/schema.cjs +27 -9
  8. package/dist/cli/config/schema.d.ts +18 -4
  9. package/dist/cli/config/schema.js +23 -8
  10. package/dist/cli/core/agentInvoker.cjs +49 -11
  11. package/dist/cli/core/agentInvoker.js +49 -11
  12. package/dist/cli/services/skillRepository.cjs +155 -69
  13. package/dist/cli/services/skillRepository.d.ts +7 -2
  14. package/dist/cli/services/skillRepository.js +155 -69
  15. package/dist/cli/services/skillService.cjs +93 -26
  16. package/dist/cli/services/skillService.d.ts +7 -0
  17. package/dist/cli/services/skillService.js +96 -29
  18. package/dist/cli/types/skill.d.ts +8 -3
  19. package/dist/skills/activation.cjs +92 -0
  20. package/dist/skills/activation.d.ts +12 -0
  21. package/dist/skills/activation.js +58 -0
  22. package/dist/skills/bin-requirements.cjs +63 -0
  23. package/dist/skills/bin-requirements.d.ts +3 -0
  24. package/dist/skills/bin-requirements.js +26 -0
  25. package/dist/skills/metadata.cjs +141 -0
  26. package/dist/skills/metadata.d.ts +29 -0
  27. package/dist/skills/metadata.js +104 -0
  28. package/dist/skills/overlay.cjs +75 -0
  29. package/dist/skills/overlay.d.ts +2 -0
  30. package/dist/skills/overlay.js +38 -0
  31. package/dist/tests/cli-config-loader.test.cjs +7 -3
  32. package/dist/tests/cli-config-loader.test.js +7 -3
  33. package/dist/tests/cli-init.test.cjs +54 -0
  34. package/dist/tests/cli-init.test.js +54 -0
  35. package/dist/tests/config-json-schema.test.cjs +12 -0
  36. package/dist/tests/config-json-schema.test.js +12 -0
  37. package/dist/tests/skill-activation.test.cjs +86 -0
  38. package/dist/tests/skill-activation.test.d.ts +1 -0
  39. package/dist/tests/skill-activation.test.js +80 -0
  40. package/dist/tests/skill-metadata.test.cjs +119 -0
  41. package/dist/tests/skill-metadata.test.d.ts +1 -0
  42. package/dist/tests/skill-metadata.test.js +113 -0
  43. package/dist/tests/skill-repository.test.cjs +363 -0
  44. package/dist/tests/skill-repository.test.js +363 -0
  45. package/package.json +4 -4
  46. package/skills/gog/SKILL.md +1 -1
  47. package/skills/weather/SKILL.md +1 -1
  48. 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
- const skillMdPath = `/repos/${this.owner}/${this.repo}/contents/skills/${skillName}/SKILL.md`;
63
- const skillMd = await this.fetchGitHub(skillMdPath);
64
- if ("file" !== skillMd.type || !skillMd.content) throw new Error("SKILL.md not found or invalid");
65
- const content = Buffer.from(skillMd.content, "base64").toString("utf-8");
66
- const metadata = this.parseSkillMetadata(content);
67
- return metadata;
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 frontmatterRegex = /^---\s*\n([\s\S]*?)\n---/;
104
- const match = content.match(frontmatterRegex);
105
- if (!match) throw new Error("Invalid SKILL.md format: missing YAML frontmatter");
106
- const frontmatter = match[1];
107
- const metadata = {
108
- name: "",
109
- description: ""
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
- const files = new Map();
145
- await this.downloadDirectory(`skills/${skillName}`, files, skillName);
146
- return files;
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 downloadDirectory(path, files, skillName) {
190
- const contents = await this.fetchGitHub(`/repos/${this.owner}/${this.repo}/contents/${path}`);
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 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
- });
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
- logger.warn(`Could not read skill ${item.name}`, error instanceof Error ? error.message : String(error));
278
+ clawhubError = error;
279
+ logger.warn(`Failed to list ClawHub skills in hybrid mode: ${error instanceof Error ? error.message : String(error)}`);
245
280
  }
246
- return skills;
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, "owner", void 0);
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 || "github";
256
- this.owner = options.repositoryOwner || "anthropics";
257
- this.repo = options.repositoryName || "skills";
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
- if ("interactive" === this.outputManager.getMode()) console.log(`\n✓ Successfully installed skill ${skillName} to ${skillPath}`);
150
- else this.outputManager.emitEvent({
151
- type: "skill-install-complete",
152
- skill: skillName,
153
- path: skillPath,
154
- timestamp: new Date().toISOString()
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 frontmatterRegex = /^---\s*\n([\s\S]*?)\n---/;
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
- if ("interactive" === this.outputManager.getMode()) console.log(`\n✓ Successfully installed skill ${skillName} to ${skillPath}`);
122
- else this.outputManager.emitEvent({
123
- type: "skill-install-complete",
124
- skill: skillName,
125
- path: skillPath,
126
- timestamp: new Date().toISOString()
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 = resolve(root);
292
- const filePath = resolve(rootResolved, normalized);
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 frontmatterRegex = /^---\s*\n([\s\S]*?)\n---/;
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, 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;