@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
@@ -59,6 +59,8 @@ const hooks_cjs_namespaceObject = require("../../agent/middleware/hooks.cjs");
59
59
  const media_compat_cjs_namespaceObject = require("../../agent/middleware/media-compat.cjs");
60
60
  const terminal_session_manager_cjs_namespaceObject = require("../../agent/tools/terminal_session_manager.cjs");
61
61
  const uiRegistry_cjs_namespaceObject = require("../../agent/uiRegistry.cjs");
62
+ const activation_cjs_namespaceObject = require("../../skills/activation.cjs");
63
+ const overlay_cjs_namespaceObject = require("../../skills/overlay.cjs");
62
64
  const agentLoader_cjs_namespaceObject = require("../../agent/config/agentLoader.cjs");
63
65
  const loader_cjs_namespaceObject = require("../config/loader.cjs");
64
66
  function _define_property(obj, key, value) {
@@ -322,6 +324,7 @@ class AgentInvoker {
322
324
  let activeToolName = null;
323
325
  let lastToolName = null;
324
326
  let rootLangGraphRunId;
327
+ const skillOverlayDirectories = [];
325
328
  const isCancelled = ()=>options?.signal?.aborted === true;
326
329
  try {
327
330
  const hookSessionId = sessionId || (0, external_uuid_namespaceObject.v4)();
@@ -412,9 +415,8 @@ class AgentInvoker {
412
415
  }
413
416
  const checkpointer = this.sessionManager?.getCheckpointer();
414
417
  const bundledSkillsPath = (0, uiRegistry_cjs_namespaceObject.getBundledSkillsPath)();
418
+ const workspaceSkillsPath = (0, external_node_path_namespaceObject.join)(this.workspace, normalizedSkillsDirectory);
415
419
  const skillsSources = [];
416
- if ((0, external_node_fs_namespaceObject.existsSync)(bundledSkillsPath)) skillsSources.push("/skills-bundled/");
417
- skillsSources.push(skillsVirtualPath);
418
420
  const backendOverrides = {
419
421
  "/memories/": new external_deepagents_namespaceObject.FilesystemBackend({
420
422
  rootDir: (0, external_node_path_namespaceObject.join)(this.workspace, this.configDir, "memories"),
@@ -430,15 +432,46 @@ class AgentInvoker {
430
432
  rootDir: executionWorkspace,
431
433
  virtualMode: true
432
434
  });
433
- const workspaceSkillsPath = (0, external_node_path_namespaceObject.join)(this.workspace, normalizedSkillsDirectory);
434
- if ((0, external_node_fs_namespaceObject.existsSync)(workspaceSkillsPath)) backendOverrides[skillsVirtualPath] = new external_deepagents_namespaceObject.FilesystemBackend({
435
- rootDir: workspaceSkillsPath,
436
- virtualMode: true
437
- });
438
- if ((0, external_node_fs_namespaceObject.existsSync)(bundledSkillsPath)) backendOverrides["/skills-bundled/"] = new external_deepagents_namespaceObject.FilesystemBackend({
439
- rootDir: bundledSkillsPath,
440
- virtualMode: true
441
- });
435
+ if ((0, external_node_fs_namespaceObject.existsSync)(workspaceSkillsPath)) {
436
+ const workspaceActivation = await (0, activation_cjs_namespaceObject.resolveSkillActivation)(workspaceSkillsPath);
437
+ if (workspaceActivation.inactiveSkills.length > 0) {
438
+ const summary = workspaceActivation.inactiveSkills.map((entry)=>`${entry.name} (missing: ${entry.missingBins.join(", ")})`).join("; ");
439
+ this.logger.info(`Inactive workspace skills: ${summary}`);
440
+ }
441
+ let workspaceOverlayRoot = workspaceSkillsPath;
442
+ try {
443
+ const workspaceOverlay = await (0, overlay_cjs_namespaceObject.createSkillOverlayDirectory)(workspaceSkillsPath, workspaceActivation.activeSkillNames);
444
+ workspaceOverlayRoot = workspaceOverlay;
445
+ skillOverlayDirectories.push(workspaceOverlay);
446
+ } catch (error) {
447
+ this.logger.debug("Failed to build workspace skill overlay; falling back to unfiltered skills directory", error);
448
+ }
449
+ backendOverrides[skillsVirtualPath] = new external_deepagents_namespaceObject.FilesystemBackend({
450
+ rootDir: workspaceOverlayRoot,
451
+ virtualMode: true
452
+ });
453
+ skillsSources.push(skillsVirtualPath);
454
+ }
455
+ if ((0, external_node_fs_namespaceObject.existsSync)(bundledSkillsPath)) {
456
+ const bundledActivation = await (0, activation_cjs_namespaceObject.resolveSkillActivation)(bundledSkillsPath);
457
+ if (bundledActivation.inactiveSkills.length > 0) {
458
+ const summary = bundledActivation.inactiveSkills.map((entry)=>`${entry.name} (missing: ${entry.missingBins.join(", ")})`).join("; ");
459
+ this.logger.info(`Inactive bundled skills: ${summary}`);
460
+ }
461
+ let bundledOverlayRoot = bundledSkillsPath;
462
+ try {
463
+ const bundledOverlay = await (0, overlay_cjs_namespaceObject.createSkillOverlayDirectory)(bundledSkillsPath, bundledActivation.activeSkillNames);
464
+ bundledOverlayRoot = bundledOverlay;
465
+ skillOverlayDirectories.push(bundledOverlay);
466
+ } catch (error) {
467
+ this.logger.debug("Failed to build bundled skill overlay; falling back to unfiltered skills directory", error);
468
+ }
469
+ backendOverrides["/skills-bundled/"] = new external_deepagents_namespaceObject.FilesystemBackend({
470
+ rootDir: bundledOverlayRoot,
471
+ virtualMode: true
472
+ });
473
+ skillsSources.push("/skills-bundled/");
474
+ }
442
475
  if (outputMount.virtualPath && outputMount.absolutePath) backendOverrides[outputMount.virtualPath] = new external_deepagents_namespaceObject.FilesystemBackend({
443
476
  rootDir: outputMount.absolutePath,
444
477
  virtualMode: true
@@ -576,6 +609,11 @@ class AgentInvoker {
576
609
  this.outputManager.emitAgentError(errorWithToolContext);
577
610
  throw error;
578
611
  } finally{
612
+ for (const overlayDirectory of skillOverlayDirectories)try {
613
+ await (0, overlay_cjs_namespaceObject.removeSkillOverlayDirectory)(overlayDirectory);
614
+ } catch (error) {
615
+ this.logger.debug(`Failed to cleanup skill overlay directory ${overlayDirectory}`, error);
616
+ }
579
617
  if (this.mcpManager) {
580
618
  this.logger.debug("Cleaning up MCP client");
581
619
  await this.mcpManager.cleanup();
@@ -10,6 +10,8 @@ import { createHooksMiddleware } from "../../agent/middleware/hooks.js";
10
10
  import { mediaCompatibilityMiddleware } from "../../agent/middleware/media-compat.js";
11
11
  import { getSharedTerminalSessionManager } from "../../agent/tools/terminal_session_manager.js";
12
12
  import { getBundledSkillsPath } from "../../agent/uiRegistry.js";
13
+ import { resolveSkillActivation } from "../../skills/activation.js";
14
+ import { createSkillOverlayDirectory, removeSkillOverlayDirectory } from "../../skills/overlay.js";
13
15
  import { AgentLoader } from "../../agent/config/agentLoader.js";
14
16
  import { WingmanConfigLoader } from "../config/loader.js";
15
17
  function _define_property(obj, key, value) {
@@ -273,6 +275,7 @@ class AgentInvoker {
273
275
  let activeToolName = null;
274
276
  let lastToolName = null;
275
277
  let rootLangGraphRunId;
278
+ const skillOverlayDirectories = [];
276
279
  const isCancelled = ()=>options?.signal?.aborted === true;
277
280
  try {
278
281
  const hookSessionId = sessionId || v4();
@@ -363,9 +366,8 @@ class AgentInvoker {
363
366
  }
364
367
  const checkpointer = this.sessionManager?.getCheckpointer();
365
368
  const bundledSkillsPath = getBundledSkillsPath();
369
+ const workspaceSkillsPath = join(this.workspace, normalizedSkillsDirectory);
366
370
  const skillsSources = [];
367
- if (existsSync(bundledSkillsPath)) skillsSources.push("/skills-bundled/");
368
- skillsSources.push(skillsVirtualPath);
369
371
  const backendOverrides = {
370
372
  "/memories/": new FilesystemBackend({
371
373
  rootDir: join(this.workspace, this.configDir, "memories"),
@@ -381,15 +383,46 @@ class AgentInvoker {
381
383
  rootDir: executionWorkspace,
382
384
  virtualMode: true
383
385
  });
384
- const workspaceSkillsPath = join(this.workspace, normalizedSkillsDirectory);
385
- if (existsSync(workspaceSkillsPath)) backendOverrides[skillsVirtualPath] = new FilesystemBackend({
386
- rootDir: workspaceSkillsPath,
387
- virtualMode: true
388
- });
389
- if (existsSync(bundledSkillsPath)) backendOverrides["/skills-bundled/"] = new FilesystemBackend({
390
- rootDir: bundledSkillsPath,
391
- virtualMode: true
392
- });
386
+ if (existsSync(workspaceSkillsPath)) {
387
+ const workspaceActivation = await resolveSkillActivation(workspaceSkillsPath);
388
+ if (workspaceActivation.inactiveSkills.length > 0) {
389
+ const summary = workspaceActivation.inactiveSkills.map((entry)=>`${entry.name} (missing: ${entry.missingBins.join(", ")})`).join("; ");
390
+ this.logger.info(`Inactive workspace skills: ${summary}`);
391
+ }
392
+ let workspaceOverlayRoot = workspaceSkillsPath;
393
+ try {
394
+ const workspaceOverlay = await createSkillOverlayDirectory(workspaceSkillsPath, workspaceActivation.activeSkillNames);
395
+ workspaceOverlayRoot = workspaceOverlay;
396
+ skillOverlayDirectories.push(workspaceOverlay);
397
+ } catch (error) {
398
+ this.logger.debug("Failed to build workspace skill overlay; falling back to unfiltered skills directory", error);
399
+ }
400
+ backendOverrides[skillsVirtualPath] = new FilesystemBackend({
401
+ rootDir: workspaceOverlayRoot,
402
+ virtualMode: true
403
+ });
404
+ skillsSources.push(skillsVirtualPath);
405
+ }
406
+ if (existsSync(bundledSkillsPath)) {
407
+ const bundledActivation = await resolveSkillActivation(bundledSkillsPath);
408
+ if (bundledActivation.inactiveSkills.length > 0) {
409
+ const summary = bundledActivation.inactiveSkills.map((entry)=>`${entry.name} (missing: ${entry.missingBins.join(", ")})`).join("; ");
410
+ this.logger.info(`Inactive bundled skills: ${summary}`);
411
+ }
412
+ let bundledOverlayRoot = bundledSkillsPath;
413
+ try {
414
+ const bundledOverlay = await createSkillOverlayDirectory(bundledSkillsPath, bundledActivation.activeSkillNames);
415
+ bundledOverlayRoot = bundledOverlay;
416
+ skillOverlayDirectories.push(bundledOverlay);
417
+ } catch (error) {
418
+ this.logger.debug("Failed to build bundled skill overlay; falling back to unfiltered skills directory", error);
419
+ }
420
+ backendOverrides["/skills-bundled/"] = new FilesystemBackend({
421
+ rootDir: bundledOverlayRoot,
422
+ virtualMode: true
423
+ });
424
+ skillsSources.push("/skills-bundled/");
425
+ }
393
426
  if (outputMount.virtualPath && outputMount.absolutePath) backendOverrides[outputMount.virtualPath] = new FilesystemBackend({
394
427
  rootDir: outputMount.absolutePath,
395
428
  virtualMode: true
@@ -527,6 +560,11 @@ class AgentInvoker {
527
560
  this.outputManager.emitAgentError(errorWithToolContext);
528
561
  throw error;
529
562
  } finally{
563
+ for (const overlayDirectory of skillOverlayDirectories)try {
564
+ await removeSkillOverlayDirectory(overlayDirectory);
565
+ } catch (error) {
566
+ this.logger.debug(`Failed to cleanup skill overlay directory ${overlayDirectory}`, error);
567
+ }
530
568
  if (this.mcpManager) {
531
569
  this.logger.debug("Cleaning up MCP client");
532
570
  await this.mcpManager.cleanup();
@@ -27,6 +27,7 @@ __webpack_require__.d(__webpack_exports__, {
27
27
  SkillRepository: ()=>SkillRepository
28
28
  });
29
29
  const external_logger_cjs_namespaceObject = require("../../logger.cjs");
30
+ const metadata_cjs_namespaceObject = require("../../skills/metadata.cjs");
30
31
  function _define_property(obj, key, value) {
31
32
  if (key in obj) Object.defineProperty(obj, key, {
32
33
  value: value,
@@ -63,7 +64,8 @@ class SkillRepository {
63
64
  async listAvailableSkills() {
64
65
  try {
65
66
  if ("clawhub" === this.provider) return await this.listSkillsFromClawhub();
66
- return await this.listSkillsFromGitHub();
67
+ if ("github" === this.provider) return await this.listSkillsFromGitHub();
68
+ return await this.listSkillsFromHybrid();
67
69
  } catch (error) {
68
70
  if (error instanceof Error) throw new Error(`Failed to list skills: ${error.message}`);
69
71
  throw error;
@@ -85,19 +87,39 @@ class SkillRepository {
85
87
  return Buffer.from(arrayBuffer);
86
88
  }
87
89
  async getSkillMetadata(skillName) {
88
- if ("clawhub" === this.provider) return await this.getClawhubSkillMetadata(skillName);
89
90
  try {
90
- const skillMdPath = `/repos/${this.owner}/${this.repo}/contents/skills/${skillName}/SKILL.md`;
91
- const skillMd = await this.fetchGitHub(skillMdPath);
92
- if ("file" !== skillMd.type || !skillMd.content) throw new Error("SKILL.md not found or invalid");
93
- const content = Buffer.from(skillMd.content, "base64").toString("utf-8");
94
- const metadata = this.parseSkillMetadata(content);
95
- return metadata;
91
+ if ("clawhub" === this.provider) return await this.getClawhubSkillMetadata(skillName);
92
+ if ("github" === this.provider) {
93
+ const repository = await this.resolveGitHubRepositoryForSkill(skillName);
94
+ return await this.getGitHubSkillMetadata(skillName, repository);
95
+ }
96
+ return await this.getHybridSkillMetadata(skillName);
96
97
  } catch (error) {
97
98
  if (error instanceof Error) throw new Error(`Failed to fetch skill metadata for ${skillName}: ${error.message}`);
98
99
  throw error;
99
100
  }
100
101
  }
102
+ async getGitHubSkillMetadata(skillName, repository) {
103
+ const skillMdPath = `/repos/${repository.owner}/${repository.name}/contents/skills/${skillName}/SKILL.md`;
104
+ const skillMd = await this.fetchGitHub(skillMdPath);
105
+ if ("file" !== skillMd.type || !skillMd.content) throw new Error(`SKILL.md not found or invalid in ${repository.owner}/${repository.name}`);
106
+ const content = Buffer.from(skillMd.content, "base64").toString("utf-8");
107
+ return this.parseSkillMetadata(content);
108
+ }
109
+ async resolveGitHubRepositoryForSkill(skillName) {
110
+ const repositories = this.getGitHubRepositories();
111
+ for(let index = repositories.length - 1; index >= 0; index -= 1){
112
+ const repository = repositories[index];
113
+ try {
114
+ await this.fetchGitHub(`/repos/${repository.owner}/${repository.name}/contents/skills/${skillName}/SKILL.md`);
115
+ return repository;
116
+ } catch (error) {
117
+ if (error instanceof Error && error.message.includes("Resource not found")) continue;
118
+ throw error;
119
+ }
120
+ }
121
+ throw new Error(`Skill '${skillName}' not found in configured GitHub repositories: ${repositories.map((repository)=>`${repository.owner}/${repository.name}`).join(", ")}`);
122
+ }
101
123
  async getClawhubSkillMetadata(skillName) {
102
124
  try {
103
125
  const detail = await this.fetchJson(`${this.clawhubBaseUrl}/api/v1/skills/${encodeURIComponent(skillName)}`, {
@@ -127,51 +149,50 @@ class SkillRepository {
127
149
  throw error;
128
150
  }
129
151
  }
152
+ async getHybridSkillMetadata(skillName) {
153
+ let githubError;
154
+ try {
155
+ const repository = await this.resolveGitHubRepositoryForSkill(skillName);
156
+ return await this.getGitHubSkillMetadata(skillName, repository);
157
+ } catch (error) {
158
+ githubError = error;
159
+ logger.debug(`Falling back to ClawHub metadata lookup for '${skillName}' after GitHub error: ${error instanceof Error ? error.message : String(error)}`);
160
+ }
161
+ try {
162
+ return await this.getClawhubSkillMetadata(skillName);
163
+ } catch (clawhubError) {
164
+ throw new Error(`GitHub error: ${githubError instanceof Error ? githubError.message : String(githubError)}; ClawHub error: ${clawhubError instanceof Error ? clawhubError.message : String(clawhubError)}`);
165
+ }
166
+ }
130
167
  parseSkillMetadata(content) {
131
- const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---/;
132
- const match = content.match(frontmatterRegex);
133
- if (!match) throw new Error("Invalid SKILL.md format: missing YAML frontmatter");
134
- const frontmatter = match[1];
135
- const metadata = {
136
- name: "",
137
- description: ""
168
+ const parsed = (0, metadata_cjs_namespaceObject.parseSkillFrontmatter)(content);
169
+ return {
170
+ name: parsed.name,
171
+ description: parsed.description,
172
+ ...parsed.license ? {
173
+ license: parsed.license
174
+ } : {},
175
+ ...parsed.compatibility ? {
176
+ compatibility: parsed.compatibility
177
+ } : {},
178
+ ...parsed.allowedTools.length > 0 ? {
179
+ allowedTools: parsed.allowedTools
180
+ } : {},
181
+ ...parsed.metadata ? {
182
+ metadata: parsed.metadata
183
+ } : {}
138
184
  };
139
- const lines = frontmatter.split("\n");
140
- for (const line of lines){
141
- const colonIndex = line.indexOf(":");
142
- if (-1 === colonIndex) continue;
143
- const key = line.substring(0, colonIndex).trim();
144
- const value = line.substring(colonIndex + 1).trim();
145
- switch(key){
146
- case "name":
147
- metadata.name = value;
148
- break;
149
- case "description":
150
- metadata.description = value;
151
- break;
152
- case "license":
153
- metadata.license = value;
154
- break;
155
- case "compatibility":
156
- metadata.compatibility = value;
157
- break;
158
- case "allowed-tools":
159
- metadata.allowedTools = value;
160
- break;
161
- }
162
- }
163
- if (!metadata.name) throw new Error("Invalid SKILL.md: missing required field 'name'");
164
- if (!metadata.description) throw new Error("Invalid SKILL.md: missing required field 'description'");
165
- const nameRegex = /^[a-z0-9]+(-[a-z0-9]+)*$/;
166
- if (!nameRegex.test(metadata.name)) throw new Error(`Invalid skill name '${metadata.name}': must be lowercase alphanumeric with hyphens only`);
167
- return metadata;
168
185
  }
169
186
  async downloadSkill(skillName) {
170
- if ("clawhub" === this.provider) return await this.downloadSkillFromClawhub(skillName);
171
187
  try {
172
- const files = new Map();
173
- await this.downloadDirectory(`skills/${skillName}`, files, skillName);
174
- return files;
188
+ if ("clawhub" === this.provider) return await this.downloadSkillFromClawhub(skillName);
189
+ if ("github" === this.provider) {
190
+ const repository = await this.resolveGitHubRepositoryForSkill(skillName);
191
+ const files = new Map();
192
+ await this.downloadDirectory(`skills/${skillName}`, files, skillName, repository);
193
+ return files;
194
+ }
195
+ return await this.downloadHybridSkill(skillName);
175
196
  } catch (error) {
176
197
  if (error instanceof Error) throw new Error(`Failed to download skill ${skillName}: ${error.message}`);
177
198
  throw error;
@@ -214,8 +235,25 @@ class SkillRepository {
214
235
  throw error;
215
236
  }
216
237
  }
217
- async downloadDirectory(path, files, skillName) {
218
- const contents = await this.fetchGitHub(`/repos/${this.owner}/${this.repo}/contents/${path}`);
238
+ async downloadHybridSkill(skillName) {
239
+ let githubError;
240
+ try {
241
+ const repository = await this.resolveGitHubRepositoryForSkill(skillName);
242
+ const files = new Map();
243
+ await this.downloadDirectory(`skills/${skillName}`, files, skillName, repository);
244
+ return files;
245
+ } catch (error) {
246
+ githubError = error;
247
+ logger.debug(`Falling back to ClawHub download for '${skillName}' after GitHub error: ${error instanceof Error ? error.message : String(error)}`);
248
+ }
249
+ try {
250
+ return await this.downloadSkillFromClawhub(skillName);
251
+ } catch (clawhubError) {
252
+ throw new Error(`GitHub error: ${githubError instanceof Error ? githubError.message : String(githubError)}; ClawHub error: ${clawhubError instanceof Error ? clawhubError.message : String(clawhubError)}`);
253
+ }
254
+ }
255
+ async downloadDirectory(path, files, skillName, repository) {
256
+ const contents = await this.fetchGitHub(`/repos/${repository.owner}/${repository.name}/contents/${path}`);
219
257
  for (const item of contents)if ("file" === item.type) if (item.content) {
220
258
  const content = Buffer.from(item.content, "base64");
221
259
  const relativePath = item.path.replace(`skills/${skillName}/`, "");
@@ -228,7 +266,7 @@ class SkillRepository {
228
266
  files.set(relativePath, content);
229
267
  }
230
268
  }
231
- else if ("dir" === item.type) await this.downloadDirectory(item.path, files, skillName);
269
+ else if ("dir" === item.type) await this.downloadDirectory(item.path, files, skillName, repository);
232
270
  }
233
271
  async listSkillsFromClawhub() {
234
272
  const allSkills = [];
@@ -257,32 +295,80 @@ class SkillRepository {
257
295
  }while (cursor);
258
296
  return allSkills;
259
297
  }
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
- });
298
+ async listSkillsFromHybrid() {
299
+ let clawhubSkills = [];
300
+ let githubSkills = [];
301
+ let clawhubError;
302
+ let githubError;
303
+ try {
304
+ clawhubSkills = await this.listSkillsFromClawhub();
271
305
  } catch (error) {
272
- logger.warn(`Could not read skill ${item.name}`, error instanceof Error ? error.message : String(error));
306
+ clawhubError = error;
307
+ logger.warn(`Failed to list ClawHub skills in hybrid mode: ${error instanceof Error ? error.message : String(error)}`);
273
308
  }
274
- return skills;
309
+ try {
310
+ githubSkills = await this.listSkillsFromGitHub();
311
+ } catch (error) {
312
+ githubError = error;
313
+ logger.warn(`Failed to list GitHub skills in hybrid mode: ${error instanceof Error ? error.message : String(error)}`);
314
+ }
315
+ 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"}`);
316
+ const mergedSkills = new Map();
317
+ for (const skill of clawhubSkills)mergedSkills.set(skill.name, skill);
318
+ for (const skill of githubSkills){
319
+ if (mergedSkills.has(skill.name)) mergedSkills.delete(skill.name);
320
+ mergedSkills.set(skill.name, skill);
321
+ }
322
+ return [
323
+ ...mergedSkills.values()
324
+ ];
325
+ }
326
+ async listSkillsFromGitHub() {
327
+ const mergedSkills = new Map();
328
+ for (const repository of this.getGitHubRepositories()){
329
+ const contents = await this.fetchGitHub(`/repos/${repository.owner}/${repository.name}/contents/skills`);
330
+ for (const item of contents)if ("dir" === item.type) try {
331
+ const metadata = await this.getGitHubSkillMetadata(item.name, repository);
332
+ if (mergedSkills.has(item.name)) mergedSkills.delete(item.name);
333
+ mergedSkills.set(item.name, {
334
+ name: item.name,
335
+ description: metadata.description || "No description",
336
+ path: item.path,
337
+ metadata
338
+ });
339
+ } catch (error) {
340
+ logger.warn(`Could not read skill ${item.name} from ${repository.owner}/${repository.name}`, error instanceof Error ? error.message : String(error));
341
+ }
342
+ }
343
+ return [
344
+ ...mergedSkills.values()
345
+ ];
346
+ }
347
+ getGitHubRepositories() {
348
+ if (this.repositories.length > 0) return this.repositories;
349
+ throw new Error("No GitHub skill repositories configured. Set skills.repositories or the legacy skills.repositoryOwner + skills.repositoryName fields.");
275
350
  }
276
351
  constructor(options = {}){
277
352
  _define_property(this, "githubBaseUrl", "https://api.github.com");
278
- _define_property(this, "owner", void 0);
279
- _define_property(this, "repo", void 0);
353
+ _define_property(this, "repositories", void 0);
280
354
  _define_property(this, "token", void 0);
281
355
  _define_property(this, "provider", void 0);
282
356
  _define_property(this, "clawhubBaseUrl", void 0);
283
- this.provider = options.provider || "github";
284
- this.owner = options.repositoryOwner || "anthropics";
285
- this.repo = options.repositoryName || "skills";
357
+ this.provider = options.provider || "hybrid";
358
+ const normalizedRepositories = (options.repositories || []).map((repository)=>({
359
+ owner: repository.owner.trim(),
360
+ name: repository.name.trim()
361
+ })).filter((repository)=>repository.owner && repository.name);
362
+ const legacyOwner = options.repositoryOwner?.trim();
363
+ const legacyName = options.repositoryName?.trim();
364
+ if (normalizedRepositories.length > 0) this.repositories = normalizedRepositories;
365
+ else if (legacyOwner && legacyName) this.repositories = [
366
+ {
367
+ owner: legacyOwner,
368
+ name: legacyName
369
+ }
370
+ ];
371
+ else this.repositories = [];
286
372
  this.token = options.githubToken || process.env.GITHUB_TOKEN || void 0;
287
373
  this.clawhubBaseUrl = (options.clawhubBaseUrl || "https://clawhub.ai").replace(/\/+$/, "");
288
374
  }
@@ -4,8 +4,7 @@ import type { SkillInfo, SkillMetadata, SkillRepositoryOptions } from "../types/
4
4
  */
5
5
  export declare class SkillRepository {
6
6
  private readonly githubBaseUrl;
7
- private readonly owner;
8
- private readonly repo;
7
+ private readonly repositories;
9
8
  private readonly token?;
10
9
  private readonly provider;
11
10
  private readonly clawhubBaseUrl;
@@ -24,7 +23,10 @@ export declare class SkillRepository {
24
23
  * Get skill metadata by fetching and parsing SKILL.md
25
24
  */
26
25
  getSkillMetadata(skillName: string): Promise<SkillMetadata>;
26
+ private getGitHubSkillMetadata;
27
+ private resolveGitHubRepositoryForSkill;
27
28
  private getClawhubSkillMetadata;
29
+ private getHybridSkillMetadata;
28
30
  /**
29
31
  * Parse SKILL.md content to extract YAML frontmatter
30
32
  */
@@ -34,10 +36,13 @@ export declare class SkillRepository {
34
36
  */
35
37
  downloadSkill(skillName: string): Promise<Map<string, string | Buffer>>;
36
38
  private downloadSkillFromClawhub;
39
+ private downloadHybridSkill;
37
40
  /**
38
41
  * Recursively download all files in a directory
39
42
  */
40
43
  private downloadDirectory;
41
44
  private listSkillsFromClawhub;
45
+ private listSkillsFromHybrid;
42
46
  private listSkillsFromGitHub;
47
+ private getGitHubRepositories;
43
48
  }