reskill 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/__integration__/helpers.d.ts +14 -0
- package/dist/cli/commands/__integration__/helpers.d.ts.map +1 -1
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/publish.d.ts.map +1 -1
- package/dist/cli/index.js +365 -429
- package/dist/core/skill-manager.d.ts +7 -0
- package/dist/core/skill-manager.d.ts.map +1 -1
- package/dist/core/skill-validator.d.ts +11 -13
- package/dist/core/skill-validator.d.ts.map +1 -1
- package/dist/index.js +301 -305
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3147,6 +3147,236 @@ class RegistryResolver {
|
|
|
3147
3147
|
return destDir;
|
|
3148
3148
|
}
|
|
3149
3149
|
}
|
|
3150
|
+
/**
|
|
3151
|
+
* Skill Parser - SKILL.md parser
|
|
3152
|
+
*
|
|
3153
|
+
* Following agentskills.io specification: https://agentskills.io/specification
|
|
3154
|
+
*
|
|
3155
|
+
* SKILL.md format requirements:
|
|
3156
|
+
* - YAML frontmatter containing name and description (required)
|
|
3157
|
+
* - name: max 64 characters, lowercase letters, numbers, hyphens
|
|
3158
|
+
* - description: max 1024 characters
|
|
3159
|
+
* - Optional fields: license, compatibility, metadata, allowed-tools
|
|
3160
|
+
*/ /**
|
|
3161
|
+
* Skill validation error
|
|
3162
|
+
*/ class SkillValidationError extends Error {
|
|
3163
|
+
field;
|
|
3164
|
+
constructor(message, field){
|
|
3165
|
+
super(message), this.field = field;
|
|
3166
|
+
this.name = 'SkillValidationError';
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
/**
|
|
3170
|
+
* Simple YAML frontmatter parser
|
|
3171
|
+
* Parses --- delimited YAML header
|
|
3172
|
+
*
|
|
3173
|
+
* Supports:
|
|
3174
|
+
* - Basic key: value pairs
|
|
3175
|
+
* - Multiline strings (| and >)
|
|
3176
|
+
* - Nested objects (one level deep, for metadata field)
|
|
3177
|
+
*/ function parseFrontmatter(content) {
|
|
3178
|
+
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
|
|
3179
|
+
const match = content.match(frontmatterRegex);
|
|
3180
|
+
if (!match) return {
|
|
3181
|
+
data: {},
|
|
3182
|
+
content
|
|
3183
|
+
};
|
|
3184
|
+
const yamlContent = match[1];
|
|
3185
|
+
const markdownContent = match[2];
|
|
3186
|
+
// Simple YAML parsing (supports basic key: value format and one level of nesting)
|
|
3187
|
+
const data = {};
|
|
3188
|
+
const lines = yamlContent.split('\n');
|
|
3189
|
+
let currentKey = '';
|
|
3190
|
+
let currentValue = '';
|
|
3191
|
+
let inMultiline = false;
|
|
3192
|
+
let inNestedObject = false;
|
|
3193
|
+
let nestedObject = {};
|
|
3194
|
+
for (const line of lines){
|
|
3195
|
+
const trimmedLine = line.trim();
|
|
3196
|
+
if (!trimmedLine || trimmedLine.startsWith('#')) continue;
|
|
3197
|
+
// Check if it's a nested key: value pair (indented with 2 spaces)
|
|
3198
|
+
const nestedMatch = line.match(/^ ([a-zA-Z_-]+):\s*(.*)$/);
|
|
3199
|
+
if (nestedMatch && inNestedObject) {
|
|
3200
|
+
const [, nestedKey, nestedValue] = nestedMatch;
|
|
3201
|
+
nestedObject[nestedKey] = parseYamlValue(nestedValue.trim());
|
|
3202
|
+
continue;
|
|
3203
|
+
}
|
|
3204
|
+
// Check if it's a new key: value pair (no indent)
|
|
3205
|
+
const keyValueMatch = line.match(/^([a-zA-Z_-]+):\s*(.*)$/);
|
|
3206
|
+
if (keyValueMatch && !inMultiline) {
|
|
3207
|
+
// Save previous nested object if any
|
|
3208
|
+
if (inNestedObject && currentKey) {
|
|
3209
|
+
data[currentKey] = nestedObject;
|
|
3210
|
+
nestedObject = {};
|
|
3211
|
+
inNestedObject = false;
|
|
3212
|
+
}
|
|
3213
|
+
// Save previous value
|
|
3214
|
+
if (currentKey && !inNestedObject) data[currentKey] = parseYamlValue(currentValue.trim());
|
|
3215
|
+
currentKey = keyValueMatch[1];
|
|
3216
|
+
currentValue = keyValueMatch[2];
|
|
3217
|
+
// Check if it's start of multiline string
|
|
3218
|
+
if ('|' === currentValue || '>' === currentValue) {
|
|
3219
|
+
inMultiline = true;
|
|
3220
|
+
currentValue = '';
|
|
3221
|
+
} else if ('' === currentValue) {
|
|
3222
|
+
// Empty value - might be start of nested object
|
|
3223
|
+
inNestedObject = true;
|
|
3224
|
+
nestedObject = {};
|
|
3225
|
+
}
|
|
3226
|
+
} else if (inMultiline && line.startsWith(' ')) // Multiline string continuation
|
|
3227
|
+
currentValue += (currentValue ? '\n' : '') + line.slice(2);
|
|
3228
|
+
else if (inMultiline && !line.startsWith(' ')) {
|
|
3229
|
+
// Multiline string end
|
|
3230
|
+
inMultiline = false;
|
|
3231
|
+
data[currentKey] = currentValue.trim();
|
|
3232
|
+
// Try to parse new line
|
|
3233
|
+
const newKeyMatch = line.match(/^([a-zA-Z_-]+):\s*(.*)$/);
|
|
3234
|
+
if (newKeyMatch) {
|
|
3235
|
+
currentKey = newKeyMatch[1];
|
|
3236
|
+
currentValue = newKeyMatch[2];
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
// Save last value
|
|
3241
|
+
if (inNestedObject && currentKey) data[currentKey] = nestedObject;
|
|
3242
|
+
else if (currentKey) data[currentKey] = parseYamlValue(currentValue.trim());
|
|
3243
|
+
return {
|
|
3244
|
+
data,
|
|
3245
|
+
content: markdownContent
|
|
3246
|
+
};
|
|
3247
|
+
}
|
|
3248
|
+
/**
|
|
3249
|
+
* Parse YAML value
|
|
3250
|
+
*/ function parseYamlValue(value) {
|
|
3251
|
+
if (!value) return '';
|
|
3252
|
+
// Boolean value
|
|
3253
|
+
if ('true' === value) return true;
|
|
3254
|
+
if ('false' === value) return false;
|
|
3255
|
+
// Number
|
|
3256
|
+
if (/^-?\d+$/.test(value)) return parseInt(value, 10);
|
|
3257
|
+
if (/^-?\d+\.\d+$/.test(value)) return parseFloat(value);
|
|
3258
|
+
// Remove quotes
|
|
3259
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
|
|
3260
|
+
return value;
|
|
3261
|
+
}
|
|
3262
|
+
/**
|
|
3263
|
+
* Validate skill name format
|
|
3264
|
+
*
|
|
3265
|
+
* Specification requirements:
|
|
3266
|
+
* - Max 64 characters
|
|
3267
|
+
* - Only lowercase letters, numbers, hyphens allowed
|
|
3268
|
+
* - Cannot start or end with hyphen
|
|
3269
|
+
* - Cannot contain consecutive hyphens
|
|
3270
|
+
*/ function validateSkillName(name) {
|
|
3271
|
+
if (!name) throw new SkillValidationError('Skill name is required', 'name');
|
|
3272
|
+
if (name.length > 64) throw new SkillValidationError('Skill name must be at most 64 characters', 'name');
|
|
3273
|
+
if (!/^[a-z0-9]/.test(name)) throw new SkillValidationError('Skill name must start with a lowercase letter or number', 'name');
|
|
3274
|
+
if (!/[a-z0-9]$/.test(name)) throw new SkillValidationError('Skill name must end with a lowercase letter or number', 'name');
|
|
3275
|
+
if (/--/.test(name)) throw new SkillValidationError('Skill name cannot contain consecutive hyphens', 'name');
|
|
3276
|
+
if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(name) && name.length > 1) throw new SkillValidationError('Skill name can only contain lowercase letters, numbers, and hyphens', 'name');
|
|
3277
|
+
// Single character name
|
|
3278
|
+
if (1 === name.length && !/^[a-z0-9]$/.test(name)) throw new SkillValidationError('Single character skill name must be a lowercase letter or number', 'name');
|
|
3279
|
+
}
|
|
3280
|
+
/**
|
|
3281
|
+
* Validate skill description
|
|
3282
|
+
*
|
|
3283
|
+
* Specification requirements:
|
|
3284
|
+
* - Max 1024 characters
|
|
3285
|
+
* - Angle brackets are allowed per agentskills.io spec
|
|
3286
|
+
*/ function validateSkillDescription(description) {
|
|
3287
|
+
if (!description) throw new SkillValidationError('Skill description is required', 'description');
|
|
3288
|
+
if (description.length > 1024) throw new SkillValidationError('Skill description must be at most 1024 characters', 'description');
|
|
3289
|
+
// Note: angle brackets are allowed per agentskills.io spec
|
|
3290
|
+
}
|
|
3291
|
+
/**
|
|
3292
|
+
* Parse SKILL.md content
|
|
3293
|
+
*
|
|
3294
|
+
* @param content - SKILL.md file content
|
|
3295
|
+
* @param options - Parse options
|
|
3296
|
+
* @returns Parsed skill info, or null if format is invalid
|
|
3297
|
+
* @throws SkillValidationError if validation fails in strict mode
|
|
3298
|
+
*/ function parseSkillMd(content, options = {}) {
|
|
3299
|
+
const { strict = false } = options;
|
|
3300
|
+
try {
|
|
3301
|
+
const { data, content: body } = parseFrontmatter(content);
|
|
3302
|
+
// Check required fields
|
|
3303
|
+
if (!data.name || !data.description) {
|
|
3304
|
+
if (strict) throw new SkillValidationError('SKILL.md must have name and description in frontmatter');
|
|
3305
|
+
return null;
|
|
3306
|
+
}
|
|
3307
|
+
const name = String(data.name);
|
|
3308
|
+
const description = String(data.description);
|
|
3309
|
+
// Validate field format
|
|
3310
|
+
if (strict) {
|
|
3311
|
+
validateSkillName(name);
|
|
3312
|
+
validateSkillDescription(description);
|
|
3313
|
+
}
|
|
3314
|
+
// Parse allowed-tools
|
|
3315
|
+
let allowedTools;
|
|
3316
|
+
if (data['allowed-tools']) {
|
|
3317
|
+
const toolsStr = String(data['allowed-tools']);
|
|
3318
|
+
allowedTools = toolsStr.split(/\s+/).filter(Boolean);
|
|
3319
|
+
}
|
|
3320
|
+
return {
|
|
3321
|
+
name,
|
|
3322
|
+
description,
|
|
3323
|
+
version: data.version ? String(data.version) : void 0,
|
|
3324
|
+
license: data.license ? String(data.license) : void 0,
|
|
3325
|
+
compatibility: data.compatibility ? String(data.compatibility) : void 0,
|
|
3326
|
+
metadata: data.metadata,
|
|
3327
|
+
allowedTools,
|
|
3328
|
+
content: body,
|
|
3329
|
+
rawContent: content
|
|
3330
|
+
};
|
|
3331
|
+
} catch (error) {
|
|
3332
|
+
if (error instanceof SkillValidationError) throw error;
|
|
3333
|
+
if (strict) throw new SkillValidationError(`Failed to parse SKILL.md: ${error}`);
|
|
3334
|
+
return null;
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
/**
|
|
3338
|
+
* Parse SKILL.md from file path
|
|
3339
|
+
*/ function skill_parser_parseSkillMdFile(filePath, options = {}) {
|
|
3340
|
+
if (!external_node_fs_.existsSync(filePath)) {
|
|
3341
|
+
if (options.strict) throw new SkillValidationError(`SKILL.md not found: ${filePath}`);
|
|
3342
|
+
return null;
|
|
3343
|
+
}
|
|
3344
|
+
const content = external_node_fs_.readFileSync(filePath, 'utf-8');
|
|
3345
|
+
return parseSkillMd(content, options);
|
|
3346
|
+
}
|
|
3347
|
+
/**
|
|
3348
|
+
* Parse SKILL.md from skill directory
|
|
3349
|
+
*/ function parseSkillFromDir(dirPath, options = {}) {
|
|
3350
|
+
const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dirPath, 'SKILL.md');
|
|
3351
|
+
return skill_parser_parseSkillMdFile(skillMdPath, options);
|
|
3352
|
+
}
|
|
3353
|
+
/**
|
|
3354
|
+
* Check if directory contains valid SKILL.md
|
|
3355
|
+
*/ function hasValidSkillMd(dirPath) {
|
|
3356
|
+
const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dirPath, 'SKILL.md');
|
|
3357
|
+
if (!external_node_fs_.existsSync(skillMdPath)) return false;
|
|
3358
|
+
try {
|
|
3359
|
+
const skill = skill_parser_parseSkillMdFile(skillMdPath);
|
|
3360
|
+
return null !== skill;
|
|
3361
|
+
} catch {
|
|
3362
|
+
return false;
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
/**
|
|
3366
|
+
* Generate SKILL.md content
|
|
3367
|
+
*/ function generateSkillMd(skill) {
|
|
3368
|
+
const frontmatter = [
|
|
3369
|
+
'---'
|
|
3370
|
+
];
|
|
3371
|
+
frontmatter.push(`name: ${skill.name}`);
|
|
3372
|
+
frontmatter.push(`description: ${skill.description}`);
|
|
3373
|
+
if (skill.license) frontmatter.push(`license: ${skill.license}`);
|
|
3374
|
+
if (skill.compatibility) frontmatter.push(`compatibility: ${skill.compatibility}`);
|
|
3375
|
+
if (skill.allowedTools && skill.allowedTools.length > 0) frontmatter.push(`allowed-tools: ${skill.allowedTools.join(' ')}`);
|
|
3376
|
+
frontmatter.push('---');
|
|
3377
|
+
frontmatter.push('');
|
|
3378
|
+
return frontmatter.join('\n') + skill.content;
|
|
3379
|
+
}
|
|
3150
3380
|
/**
|
|
3151
3381
|
* SkillManager - Core Skill management class
|
|
3152
3382
|
*
|
|
@@ -3237,6 +3467,24 @@ class RegistryResolver {
|
|
|
3237
3467
|
return RegistryResolver.isRegistryRef(ref);
|
|
3238
3468
|
}
|
|
3239
3469
|
/**
|
|
3470
|
+
* Get skill metadata from SKILL.md in a directory
|
|
3471
|
+
*
|
|
3472
|
+
* @param dirPath - Path to the skill directory
|
|
3473
|
+
* @returns Skill metadata (name, version, description) or null if not found
|
|
3474
|
+
*/ getSkillMetadataFromDir(dirPath) {
|
|
3475
|
+
try {
|
|
3476
|
+
const skill = parseSkillFromDir(dirPath);
|
|
3477
|
+
if (skill?.name) return {
|
|
3478
|
+
name: skill.name,
|
|
3479
|
+
version: skill.version,
|
|
3480
|
+
description: skill.description
|
|
3481
|
+
};
|
|
3482
|
+
} catch (error) {
|
|
3483
|
+
logger_logger.debug(`Failed to parse SKILL.md in ${dirPath}: ${error.message}`);
|
|
3484
|
+
}
|
|
3485
|
+
return null;
|
|
3486
|
+
}
|
|
3487
|
+
/**
|
|
3240
3488
|
* Install skill
|
|
3241
3489
|
*/ async install(ref, options = {}) {
|
|
3242
3490
|
// Detect source type and delegate to appropriate installer
|
|
@@ -3251,9 +3499,21 @@ class RegistryResolver {
|
|
|
3251
3499
|
const resolved = await this.resolver.resolve(ref);
|
|
3252
3500
|
const { parsed, repoUrl } = resolved;
|
|
3253
3501
|
const gitRef = resolved.ref; // Git reference (tag, branch, commit)
|
|
3254
|
-
|
|
3502
|
+
// Fallback name from path/repo (will be overridden by SKILL.md name if available)
|
|
3503
|
+
const fallbackName = parsed.subPath ? __WEBPACK_EXTERNAL_MODULE_node_path__.basename(parsed.subPath) : parsed.repo;
|
|
3504
|
+
// Cache first - we need to read SKILL.md to get the real name
|
|
3505
|
+
let cacheResult = await this.cache.get(parsed, gitRef);
|
|
3506
|
+
if (!cacheResult) {
|
|
3507
|
+
logger_logger.debug(`Caching from ${repoUrl}@${gitRef}`);
|
|
3508
|
+
cacheResult = await this.cache.cache(repoUrl, parsed, gitRef, gitRef);
|
|
3509
|
+
}
|
|
3510
|
+
// Get the real skill name from SKILL.md in cache
|
|
3511
|
+
const cachePath = this.cache.getCachePath(parsed, gitRef);
|
|
3512
|
+
const metadata = this.getSkillMetadataFromDir(cachePath);
|
|
3513
|
+
const skillName = metadata?.name ?? fallbackName;
|
|
3514
|
+
const semanticVersion = metadata?.version ?? gitRef;
|
|
3255
3515
|
const skillPath = this.getSkillPath(skillName);
|
|
3256
|
-
// Check if already installed
|
|
3516
|
+
// Check if already installed (using the real name from SKILL.md)
|
|
3257
3517
|
if (exists(skillPath) && !force) {
|
|
3258
3518
|
const locked = this.lockManager.get(skillName);
|
|
3259
3519
|
// Compare ref if available, fallback to version for backward compatibility
|
|
@@ -3270,26 +3530,10 @@ class RegistryResolver {
|
|
|
3270
3530
|
}
|
|
3271
3531
|
}
|
|
3272
3532
|
logger_logger["package"](`Installing ${skillName}@${gitRef}...`);
|
|
3273
|
-
// Check cache
|
|
3274
|
-
let cacheResult = await this.cache.get(parsed, gitRef);
|
|
3275
|
-
if (cacheResult) logger_logger.debug(`Using cached ${skillName}@${gitRef}`);
|
|
3276
|
-
else {
|
|
3277
|
-
logger_logger.debug(`Caching ${skillName}@${gitRef} from ${repoUrl}`);
|
|
3278
|
-
cacheResult = await this.cache.cache(repoUrl, parsed, gitRef, gitRef);
|
|
3279
|
-
}
|
|
3280
3533
|
// Copy to installation directory
|
|
3281
3534
|
ensureDir(this.getInstallDir());
|
|
3282
3535
|
if (exists(skillPath)) remove(skillPath);
|
|
3283
3536
|
await this.cache.copyTo(parsed, gitRef, skillPath);
|
|
3284
|
-
// Read semantic version from skill.json
|
|
3285
|
-
let semanticVersion = gitRef; // fallback to gitRef if no skill.json
|
|
3286
|
-
const skillJsonPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillPath, 'skill.json');
|
|
3287
|
-
if (exists(skillJsonPath)) try {
|
|
3288
|
-
const skillJson = readJson(skillJsonPath);
|
|
3289
|
-
if (skillJson.version) semanticVersion = skillJson.version;
|
|
3290
|
-
} catch {
|
|
3291
|
-
// Ignore parse errors, use gitRef as fallback
|
|
3292
|
-
}
|
|
3293
3537
|
// Update lock file (project mode only)
|
|
3294
3538
|
if (!this.isGlobal) this.lockManager.lockSkill(skillName, {
|
|
3295
3539
|
source: `${parsed.registry}:${parsed.owner}/${parsed.repo}${parsed.subPath ? `/${parsed.subPath}` : ''}`,
|
|
@@ -3320,9 +3564,21 @@ class RegistryResolver {
|
|
|
3320
3564
|
const resolved = await this.httpResolver.resolve(ref);
|
|
3321
3565
|
const { parsed, repoUrl, httpInfo } = resolved;
|
|
3322
3566
|
const version = resolved.ref || 'latest';
|
|
3323
|
-
|
|
3567
|
+
// Fallback name from URL (will be overridden by SKILL.md name if available)
|
|
3568
|
+
const fallbackName = httpInfo.skillName;
|
|
3569
|
+
// Cache first - we need to read SKILL.md to get the real name
|
|
3570
|
+
let cacheResult = await this.cache.get(parsed, version);
|
|
3571
|
+
if (!cacheResult) {
|
|
3572
|
+
logger_logger.debug(`Downloading from ${repoUrl}`);
|
|
3573
|
+
cacheResult = await this.cache.cacheFromHttp(repoUrl, parsed, version);
|
|
3574
|
+
}
|
|
3575
|
+
// Get the real skill name from SKILL.md in cache
|
|
3576
|
+
const cachePath = this.cache.getCachePath(parsed, version);
|
|
3577
|
+
const metadata = this.getSkillMetadataFromDir(cachePath);
|
|
3578
|
+
const skillName = metadata?.name ?? fallbackName;
|
|
3579
|
+
const semanticVersion = metadata?.version ?? version;
|
|
3324
3580
|
const skillPath = this.getSkillPath(skillName);
|
|
3325
|
-
// Check if already installed
|
|
3581
|
+
// Check if already installed (using the real name from SKILL.md)
|
|
3326
3582
|
if (exists(skillPath) && !force) {
|
|
3327
3583
|
const locked = this.lockManager.get(skillName);
|
|
3328
3584
|
const lockedRef = locked?.ref || locked?.version;
|
|
@@ -3338,26 +3594,10 @@ class RegistryResolver {
|
|
|
3338
3594
|
}
|
|
3339
3595
|
}
|
|
3340
3596
|
logger_logger["package"](`Installing ${skillName}@${version} from ${httpInfo.host}...`);
|
|
3341
|
-
// Check cache
|
|
3342
|
-
let cacheResult = await this.cache.get(parsed, version);
|
|
3343
|
-
if (cacheResult) logger_logger.debug(`Using cached ${skillName}@${version}`);
|
|
3344
|
-
else {
|
|
3345
|
-
logger_logger.debug(`Downloading ${skillName}@${version} from ${repoUrl}`);
|
|
3346
|
-
cacheResult = await this.cache.cacheFromHttp(repoUrl, parsed, version);
|
|
3347
|
-
}
|
|
3348
3597
|
// Copy to installation directory
|
|
3349
3598
|
ensureDir(this.getInstallDir());
|
|
3350
3599
|
if (exists(skillPath)) remove(skillPath);
|
|
3351
3600
|
await this.cache.copyTo(parsed, version, skillPath);
|
|
3352
|
-
// Read semantic version from skill.json
|
|
3353
|
-
let semanticVersion = version;
|
|
3354
|
-
const skillJsonPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillPath, 'skill.json');
|
|
3355
|
-
if (exists(skillJsonPath)) try {
|
|
3356
|
-
const skillJson = readJson(skillJsonPath);
|
|
3357
|
-
if (skillJson.version) semanticVersion = skillJson.version;
|
|
3358
|
-
} catch {
|
|
3359
|
-
// Ignore parse errors
|
|
3360
|
-
}
|
|
3361
3601
|
// Update lock file (project mode only)
|
|
3362
3602
|
if (!this.isGlobal) this.lockManager.lockSkill(skillName, {
|
|
3363
3603
|
source: `http:${httpInfo.host}/${skillName}`,
|
|
@@ -3524,19 +3764,15 @@ class RegistryResolver {
|
|
|
3524
3764
|
if (!exists(skillPath)) return null;
|
|
3525
3765
|
const isLinked = isSymlink(skillPath);
|
|
3526
3766
|
const locked = this.lockManager.get(name);
|
|
3527
|
-
|
|
3528
|
-
const
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
} catch {
|
|
3532
|
-
// Ignore parse errors
|
|
3533
|
-
}
|
|
3767
|
+
// Read metadata from SKILL.md (sole source per agentskills.io spec)
|
|
3768
|
+
const skillMd = this.getSkillMetadataFromDir(skillPath);
|
|
3769
|
+
// Version priority: locked > SKILL.md > 'unknown'
|
|
3770
|
+
const version = isLinked ? 'local' : locked?.version || skillMd?.version || 'unknown';
|
|
3534
3771
|
return {
|
|
3535
3772
|
name,
|
|
3536
3773
|
path: skillPath,
|
|
3537
|
-
version
|
|
3774
|
+
version,
|
|
3538
3775
|
source: isLinked ? getRealPath(skillPath) : locked?.source || '',
|
|
3539
|
-
metadata,
|
|
3540
3776
|
isLinked
|
|
3541
3777
|
};
|
|
3542
3778
|
}
|
|
@@ -3635,26 +3871,21 @@ class RegistryResolver {
|
|
|
3635
3871
|
const resolved = await this.resolver.resolve(ref);
|
|
3636
3872
|
const { parsed, repoUrl } = resolved;
|
|
3637
3873
|
const gitRef = resolved.ref; // Git reference (tag, branch, commit)
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
//
|
|
3874
|
+
// Fallback name from path/repo (will be overridden by SKILL.md name if available)
|
|
3875
|
+
const fallbackName = parsed.subPath ? __WEBPACK_EXTERNAL_MODULE_node_path__.basename(parsed.subPath) : parsed.repo;
|
|
3876
|
+
// Cache first - we need to read SKILL.md to get the real name
|
|
3641
3877
|
let cacheResult = await this.cache.get(parsed, gitRef);
|
|
3642
|
-
if (cacheResult)
|
|
3643
|
-
|
|
3644
|
-
logger_logger.debug(`Caching ${skillName}@${gitRef} from ${repoUrl}`);
|
|
3878
|
+
if (!cacheResult) {
|
|
3879
|
+
logger_logger.debug(`Caching from ${repoUrl}@${gitRef}`);
|
|
3645
3880
|
cacheResult = await this.cache.cache(repoUrl, parsed, gitRef, gitRef);
|
|
3646
3881
|
}
|
|
3647
3882
|
// Get cache path as source
|
|
3648
3883
|
const sourcePath = this.cache.getCachePath(parsed, gitRef);
|
|
3649
|
-
//
|
|
3650
|
-
|
|
3651
|
-
const
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
if (skillJson.version) semanticVersion = skillJson.version;
|
|
3655
|
-
} catch {
|
|
3656
|
-
// Ignore parse errors, use gitRef as fallback
|
|
3657
|
-
}
|
|
3884
|
+
// Get the real skill name from SKILL.md in cache
|
|
3885
|
+
const metadata = this.getSkillMetadataFromDir(sourcePath);
|
|
3886
|
+
const skillName = metadata?.name ?? fallbackName;
|
|
3887
|
+
const semanticVersion = metadata?.version ?? gitRef;
|
|
3888
|
+
logger_logger["package"](`Installing ${skillName}@${gitRef} to ${targetAgents.length} agent(s)...`);
|
|
3658
3889
|
// Create Installer with custom installDir from config
|
|
3659
3890
|
const defaults = this.config.getDefaults();
|
|
3660
3891
|
const installer = new Installer({
|
|
@@ -3707,26 +3938,21 @@ class RegistryResolver {
|
|
|
3707
3938
|
const resolved = await this.httpResolver.resolve(ref);
|
|
3708
3939
|
const { parsed, repoUrl, httpInfo } = resolved;
|
|
3709
3940
|
const version = resolved.ref || 'latest';
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
//
|
|
3941
|
+
// Fallback name from URL (will be overridden by SKILL.md name if available)
|
|
3942
|
+
const fallbackName = httpInfo.skillName;
|
|
3943
|
+
// Cache first - we need to read SKILL.md to get the real name
|
|
3713
3944
|
let cacheResult = await this.cache.get(parsed, version);
|
|
3714
|
-
if (cacheResult)
|
|
3715
|
-
|
|
3716
|
-
logger_logger.debug(`Downloading ${skillName}@${version} from ${repoUrl}`);
|
|
3945
|
+
if (!cacheResult) {
|
|
3946
|
+
logger_logger.debug(`Downloading from ${repoUrl}`);
|
|
3717
3947
|
cacheResult = await this.cache.cacheFromHttp(repoUrl, parsed, version);
|
|
3718
3948
|
}
|
|
3719
3949
|
// Get cache path as source
|
|
3720
3950
|
const sourcePath = this.cache.getCachePath(parsed, version);
|
|
3721
|
-
//
|
|
3722
|
-
|
|
3723
|
-
const
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
if (skillJson.version) semanticVersion = skillJson.version;
|
|
3727
|
-
} catch {
|
|
3728
|
-
// Ignore parse errors
|
|
3729
|
-
}
|
|
3951
|
+
// Get the real skill name from SKILL.md in cache
|
|
3952
|
+
const metadata = this.getSkillMetadataFromDir(sourcePath);
|
|
3953
|
+
const skillName = metadata?.name ?? fallbackName;
|
|
3954
|
+
const semanticVersion = metadata?.version ?? version;
|
|
3955
|
+
logger_logger["package"](`Installing ${skillName}@${version} from ${httpInfo.host} to ${targetAgents.length} agent(s)...`);
|
|
3730
3956
|
// Create Installer with custom installDir from config
|
|
3731
3957
|
const defaults = this.config.getDefaults();
|
|
3732
3958
|
const installer = new Installer({
|
|
@@ -3991,234 +4217,4 @@ class RegistryResolver {
|
|
|
3991
4217
|
return results;
|
|
3992
4218
|
}
|
|
3993
4219
|
}
|
|
3994
|
-
/**
|
|
3995
|
-
* Skill Parser - SKILL.md parser
|
|
3996
|
-
*
|
|
3997
|
-
* Following agentskills.io specification: https://agentskills.io/specification
|
|
3998
|
-
*
|
|
3999
|
-
* SKILL.md format requirements:
|
|
4000
|
-
* - YAML frontmatter containing name and description (required)
|
|
4001
|
-
* - name: max 64 characters, lowercase letters, numbers, hyphens
|
|
4002
|
-
* - description: max 1024 characters
|
|
4003
|
-
* - Optional fields: license, compatibility, metadata, allowed-tools
|
|
4004
|
-
*/ /**
|
|
4005
|
-
* Skill validation error
|
|
4006
|
-
*/ class SkillValidationError extends Error {
|
|
4007
|
-
field;
|
|
4008
|
-
constructor(message, field){
|
|
4009
|
-
super(message), this.field = field;
|
|
4010
|
-
this.name = 'SkillValidationError';
|
|
4011
|
-
}
|
|
4012
|
-
}
|
|
4013
|
-
/**
|
|
4014
|
-
* Simple YAML frontmatter parser
|
|
4015
|
-
* Parses --- delimited YAML header
|
|
4016
|
-
*
|
|
4017
|
-
* Supports:
|
|
4018
|
-
* - Basic key: value pairs
|
|
4019
|
-
* - Multiline strings (| and >)
|
|
4020
|
-
* - Nested objects (one level deep, for metadata field)
|
|
4021
|
-
*/ function parseFrontmatter(content) {
|
|
4022
|
-
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
|
|
4023
|
-
const match = content.match(frontmatterRegex);
|
|
4024
|
-
if (!match) return {
|
|
4025
|
-
data: {},
|
|
4026
|
-
content
|
|
4027
|
-
};
|
|
4028
|
-
const yamlContent = match[1];
|
|
4029
|
-
const markdownContent = match[2];
|
|
4030
|
-
// Simple YAML parsing (supports basic key: value format and one level of nesting)
|
|
4031
|
-
const data = {};
|
|
4032
|
-
const lines = yamlContent.split('\n');
|
|
4033
|
-
let currentKey = '';
|
|
4034
|
-
let currentValue = '';
|
|
4035
|
-
let inMultiline = false;
|
|
4036
|
-
let inNestedObject = false;
|
|
4037
|
-
let nestedObject = {};
|
|
4038
|
-
for (const line of lines){
|
|
4039
|
-
const trimmedLine = line.trim();
|
|
4040
|
-
if (!trimmedLine || trimmedLine.startsWith('#')) continue;
|
|
4041
|
-
// Check if it's a nested key: value pair (indented with 2 spaces)
|
|
4042
|
-
const nestedMatch = line.match(/^ ([a-zA-Z_-]+):\s*(.*)$/);
|
|
4043
|
-
if (nestedMatch && inNestedObject) {
|
|
4044
|
-
const [, nestedKey, nestedValue] = nestedMatch;
|
|
4045
|
-
nestedObject[nestedKey] = parseYamlValue(nestedValue.trim());
|
|
4046
|
-
continue;
|
|
4047
|
-
}
|
|
4048
|
-
// Check if it's a new key: value pair (no indent)
|
|
4049
|
-
const keyValueMatch = line.match(/^([a-zA-Z_-]+):\s*(.*)$/);
|
|
4050
|
-
if (keyValueMatch && !inMultiline) {
|
|
4051
|
-
// Save previous nested object if any
|
|
4052
|
-
if (inNestedObject && currentKey) {
|
|
4053
|
-
data[currentKey] = nestedObject;
|
|
4054
|
-
nestedObject = {};
|
|
4055
|
-
inNestedObject = false;
|
|
4056
|
-
}
|
|
4057
|
-
// Save previous value
|
|
4058
|
-
if (currentKey && !inNestedObject) data[currentKey] = parseYamlValue(currentValue.trim());
|
|
4059
|
-
currentKey = keyValueMatch[1];
|
|
4060
|
-
currentValue = keyValueMatch[2];
|
|
4061
|
-
// Check if it's start of multiline string
|
|
4062
|
-
if ('|' === currentValue || '>' === currentValue) {
|
|
4063
|
-
inMultiline = true;
|
|
4064
|
-
currentValue = '';
|
|
4065
|
-
} else if ('' === currentValue) {
|
|
4066
|
-
// Empty value - might be start of nested object
|
|
4067
|
-
inNestedObject = true;
|
|
4068
|
-
nestedObject = {};
|
|
4069
|
-
}
|
|
4070
|
-
} else if (inMultiline && line.startsWith(' ')) // Multiline string continuation
|
|
4071
|
-
currentValue += (currentValue ? '\n' : '') + line.slice(2);
|
|
4072
|
-
else if (inMultiline && !line.startsWith(' ')) {
|
|
4073
|
-
// Multiline string end
|
|
4074
|
-
inMultiline = false;
|
|
4075
|
-
data[currentKey] = currentValue.trim();
|
|
4076
|
-
// Try to parse new line
|
|
4077
|
-
const newKeyMatch = line.match(/^([a-zA-Z_-]+):\s*(.*)$/);
|
|
4078
|
-
if (newKeyMatch) {
|
|
4079
|
-
currentKey = newKeyMatch[1];
|
|
4080
|
-
currentValue = newKeyMatch[2];
|
|
4081
|
-
}
|
|
4082
|
-
}
|
|
4083
|
-
}
|
|
4084
|
-
// Save last value
|
|
4085
|
-
if (inNestedObject && currentKey) data[currentKey] = nestedObject;
|
|
4086
|
-
else if (currentKey) data[currentKey] = parseYamlValue(currentValue.trim());
|
|
4087
|
-
return {
|
|
4088
|
-
data,
|
|
4089
|
-
content: markdownContent
|
|
4090
|
-
};
|
|
4091
|
-
}
|
|
4092
|
-
/**
|
|
4093
|
-
* Parse YAML value
|
|
4094
|
-
*/ function parseYamlValue(value) {
|
|
4095
|
-
if (!value) return '';
|
|
4096
|
-
// Boolean value
|
|
4097
|
-
if ('true' === value) return true;
|
|
4098
|
-
if ('false' === value) return false;
|
|
4099
|
-
// Number
|
|
4100
|
-
if (/^-?\d+$/.test(value)) return parseInt(value, 10);
|
|
4101
|
-
if (/^-?\d+\.\d+$/.test(value)) return parseFloat(value);
|
|
4102
|
-
// Remove quotes
|
|
4103
|
-
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
|
|
4104
|
-
return value;
|
|
4105
|
-
}
|
|
4106
|
-
/**
|
|
4107
|
-
* Validate skill name format
|
|
4108
|
-
*
|
|
4109
|
-
* Specification requirements:
|
|
4110
|
-
* - Max 64 characters
|
|
4111
|
-
* - Only lowercase letters, numbers, hyphens allowed
|
|
4112
|
-
* - Cannot start or end with hyphen
|
|
4113
|
-
* - Cannot contain consecutive hyphens
|
|
4114
|
-
*/ function validateSkillName(name) {
|
|
4115
|
-
if (!name) throw new SkillValidationError('Skill name is required', 'name');
|
|
4116
|
-
if (name.length > 64) throw new SkillValidationError('Skill name must be at most 64 characters', 'name');
|
|
4117
|
-
if (!/^[a-z0-9]/.test(name)) throw new SkillValidationError('Skill name must start with a lowercase letter or number', 'name');
|
|
4118
|
-
if (!/[a-z0-9]$/.test(name)) throw new SkillValidationError('Skill name must end with a lowercase letter or number', 'name');
|
|
4119
|
-
if (/--/.test(name)) throw new SkillValidationError('Skill name cannot contain consecutive hyphens', 'name');
|
|
4120
|
-
if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(name) && name.length > 1) throw new SkillValidationError('Skill name can only contain lowercase letters, numbers, and hyphens', 'name');
|
|
4121
|
-
// Single character name
|
|
4122
|
-
if (1 === name.length && !/^[a-z0-9]$/.test(name)) throw new SkillValidationError('Single character skill name must be a lowercase letter or number', 'name');
|
|
4123
|
-
}
|
|
4124
|
-
/**
|
|
4125
|
-
* Validate skill description
|
|
4126
|
-
*
|
|
4127
|
-
* Specification requirements:
|
|
4128
|
-
* - Max 1024 characters
|
|
4129
|
-
* - Angle brackets are allowed per agentskills.io spec
|
|
4130
|
-
*/ function validateSkillDescription(description) {
|
|
4131
|
-
if (!description) throw new SkillValidationError('Skill description is required', 'description');
|
|
4132
|
-
if (description.length > 1024) throw new SkillValidationError('Skill description must be at most 1024 characters', 'description');
|
|
4133
|
-
// Note: angle brackets are allowed per agentskills.io spec
|
|
4134
|
-
}
|
|
4135
|
-
/**
|
|
4136
|
-
* Parse SKILL.md content
|
|
4137
|
-
*
|
|
4138
|
-
* @param content - SKILL.md file content
|
|
4139
|
-
* @param options - Parse options
|
|
4140
|
-
* @returns Parsed skill info, or null if format is invalid
|
|
4141
|
-
* @throws SkillValidationError if validation fails in strict mode
|
|
4142
|
-
*/ function parseSkillMd(content, options = {}) {
|
|
4143
|
-
const { strict = false } = options;
|
|
4144
|
-
try {
|
|
4145
|
-
const { data, content: body } = parseFrontmatter(content);
|
|
4146
|
-
// Check required fields
|
|
4147
|
-
if (!data.name || !data.description) {
|
|
4148
|
-
if (strict) throw new SkillValidationError('SKILL.md must have name and description in frontmatter');
|
|
4149
|
-
return null;
|
|
4150
|
-
}
|
|
4151
|
-
const name = String(data.name);
|
|
4152
|
-
const description = String(data.description);
|
|
4153
|
-
// Validate field format
|
|
4154
|
-
if (strict) {
|
|
4155
|
-
validateSkillName(name);
|
|
4156
|
-
validateSkillDescription(description);
|
|
4157
|
-
}
|
|
4158
|
-
// Parse allowed-tools
|
|
4159
|
-
let allowedTools;
|
|
4160
|
-
if (data['allowed-tools']) {
|
|
4161
|
-
const toolsStr = String(data['allowed-tools']);
|
|
4162
|
-
allowedTools = toolsStr.split(/\s+/).filter(Boolean);
|
|
4163
|
-
}
|
|
4164
|
-
return {
|
|
4165
|
-
name,
|
|
4166
|
-
description,
|
|
4167
|
-
version: data.version ? String(data.version) : void 0,
|
|
4168
|
-
license: data.license ? String(data.license) : void 0,
|
|
4169
|
-
compatibility: data.compatibility ? String(data.compatibility) : void 0,
|
|
4170
|
-
metadata: data.metadata,
|
|
4171
|
-
allowedTools,
|
|
4172
|
-
content: body,
|
|
4173
|
-
rawContent: content
|
|
4174
|
-
};
|
|
4175
|
-
} catch (error) {
|
|
4176
|
-
if (error instanceof SkillValidationError) throw error;
|
|
4177
|
-
if (strict) throw new SkillValidationError(`Failed to parse SKILL.md: ${error}`);
|
|
4178
|
-
return null;
|
|
4179
|
-
}
|
|
4180
|
-
}
|
|
4181
|
-
/**
|
|
4182
|
-
* Parse SKILL.md from file path
|
|
4183
|
-
*/ function skill_parser_parseSkillMdFile(filePath, options = {}) {
|
|
4184
|
-
if (!external_node_fs_.existsSync(filePath)) {
|
|
4185
|
-
if (options.strict) throw new SkillValidationError(`SKILL.md not found: ${filePath}`);
|
|
4186
|
-
return null;
|
|
4187
|
-
}
|
|
4188
|
-
const content = external_node_fs_.readFileSync(filePath, 'utf-8');
|
|
4189
|
-
return parseSkillMd(content, options);
|
|
4190
|
-
}
|
|
4191
|
-
/**
|
|
4192
|
-
* Parse SKILL.md from skill directory
|
|
4193
|
-
*/ function parseSkillFromDir(dirPath, options = {}) {
|
|
4194
|
-
const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dirPath, 'SKILL.md');
|
|
4195
|
-
return skill_parser_parseSkillMdFile(skillMdPath, options);
|
|
4196
|
-
}
|
|
4197
|
-
/**
|
|
4198
|
-
* Check if directory contains valid SKILL.md
|
|
4199
|
-
*/ function hasValidSkillMd(dirPath) {
|
|
4200
|
-
const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(dirPath, 'SKILL.md');
|
|
4201
|
-
if (!external_node_fs_.existsSync(skillMdPath)) return false;
|
|
4202
|
-
try {
|
|
4203
|
-
const skill = skill_parser_parseSkillMdFile(skillMdPath);
|
|
4204
|
-
return null !== skill;
|
|
4205
|
-
} catch {
|
|
4206
|
-
return false;
|
|
4207
|
-
}
|
|
4208
|
-
}
|
|
4209
|
-
/**
|
|
4210
|
-
* Generate SKILL.md content
|
|
4211
|
-
*/ function generateSkillMd(skill) {
|
|
4212
|
-
const frontmatter = [
|
|
4213
|
-
'---'
|
|
4214
|
-
];
|
|
4215
|
-
frontmatter.push(`name: ${skill.name}`);
|
|
4216
|
-
frontmatter.push(`description: ${skill.description}`);
|
|
4217
|
-
if (skill.license) frontmatter.push(`license: ${skill.license}`);
|
|
4218
|
-
if (skill.compatibility) frontmatter.push(`compatibility: ${skill.compatibility}`);
|
|
4219
|
-
if (skill.allowedTools && skill.allowedTools.length > 0) frontmatter.push(`allowed-tools: ${skill.allowedTools.join(' ')}`);
|
|
4220
|
-
frontmatter.push('---');
|
|
4221
|
-
frontmatter.push('');
|
|
4222
|
-
return frontmatter.join('\n') + skill.content;
|
|
4223
|
-
}
|
|
4224
4220
|
export { CacheManager, config_loader_ConfigLoader as ConfigLoader, DEFAULT_REGISTRIES, GitResolver, HttpResolver, Installer, LockManager, SkillManager, SkillValidationError, agents, detectInstalledAgents, generateSkillMd, getAgentConfig, getAgentSkillsDir, getAllAgentTypes, getCanonicalSkillPath, getCanonicalSkillsDir, hasValidSkillMd, isPathSafe, isValidAgentType, logger_logger as logger, parseSkillFromDir, parseSkillMd, skill_parser_parseSkillMdFile as parseSkillMdFile, sanitizeName, shortenPath, validateSkillDescription, validateSkillName };
|