@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
|
@@ -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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return
|
|
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
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
|
218
|
-
|
|
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
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
306
|
+
clawhubError = error;
|
|
307
|
+
logger.warn(`Failed to list ClawHub skills in hybrid mode: ${error instanceof Error ? error.message : String(error)}`);
|
|
273
308
|
}
|
|
274
|
-
|
|
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, "
|
|
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 || "
|
|
284
|
-
|
|
285
|
-
|
|
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
|
|
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
|
}
|