reskill 1.16.0-beta.1 → 1.17.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/README.md +56 -17
- package/README.zh-CN.md +60 -18
- package/dist/cli/commands/group.d.ts +20 -0
- package/dist/cli/commands/group.d.ts.map +1 -0
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/publish.d.ts.map +1 -1
- package/dist/cli/index.js +1564 -965
- package/dist/core/content-scanner.d.ts.map +1 -1
- package/dist/core/registry-client.d.ts +75 -1
- package/dist/core/registry-client.d.ts.map +1 -1
- package/dist/core/skill-manager.d.ts +1 -1
- package/dist/core/skill-manager.d.ts.map +1 -1
- package/dist/index.js +188 -5
- package/dist/scanner.js +1 -0
- package/dist/types/index.d.ts +27 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/group-path.d.ts +40 -0
- package/dist/utils/group-path.d.ts.map +1 -0
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -3473,6 +3473,7 @@ class RegistryClient {
|
|
|
3473
3473
|
formData.append('metadata', JSON.stringify(metadata));
|
|
3474
3474
|
if (payload.skillMd?.allowedTools) formData.append('allowed_tools', payload.skillMd.allowedTools.join(' '));
|
|
3475
3475
|
if (options.tag) formData.append('tag', options.tag);
|
|
3476
|
+
if (options.groupPath) formData.append('group_path', options.groupPath);
|
|
3476
3477
|
// Append tarball as Blob
|
|
3477
3478
|
const tarballBlob = new Blob([
|
|
3478
3479
|
tarball
|
|
@@ -3490,6 +3491,186 @@ class RegistryClient {
|
|
|
3490
3491
|
if (!response.ok) throw new RegistryError(data.error || `Publish failed: ${response.status}`, response.status, data);
|
|
3491
3492
|
return data;
|
|
3492
3493
|
}
|
|
3494
|
+
// ============================================================================
|
|
3495
|
+
// Group Methods
|
|
3496
|
+
// ============================================================================
|
|
3497
|
+
/**
|
|
3498
|
+
* Resolve a human-readable group path to its details.
|
|
3499
|
+
*
|
|
3500
|
+
* @param groupPath - Human-readable path (e.g., "kanyun/frontend")
|
|
3501
|
+
* @returns Group detail with current_user_role if authenticated
|
|
3502
|
+
* @throws RegistryError if not found or request failed
|
|
3503
|
+
*/ async resolveGroup(groupPath) {
|
|
3504
|
+
const params = new URLSearchParams({
|
|
3505
|
+
path: groupPath
|
|
3506
|
+
});
|
|
3507
|
+
const url = `${this.getApiBase()}/skill-groups/resolve?${params.toString()}`;
|
|
3508
|
+
const response = await fetch(url, {
|
|
3509
|
+
method: 'GET',
|
|
3510
|
+
headers: this.getAuthHeaders()
|
|
3511
|
+
});
|
|
3512
|
+
if (!response.ok) {
|
|
3513
|
+
const data = await response.json();
|
|
3514
|
+
throw new RegistryError(data.error || `Group not found: ${groupPath}`, response.status, data);
|
|
3515
|
+
}
|
|
3516
|
+
const body = await response.json();
|
|
3517
|
+
return body.data;
|
|
3518
|
+
}
|
|
3519
|
+
/**
|
|
3520
|
+
* List groups visible to the current user.
|
|
3521
|
+
*
|
|
3522
|
+
* @param options - Filter options (parent_id, visibility)
|
|
3523
|
+
* @returns Array of groups
|
|
3524
|
+
*/ async listGroups(options = {}) {
|
|
3525
|
+
const params = new URLSearchParams();
|
|
3526
|
+
if (options.parentId) params.set('parent_id', options.parentId);
|
|
3527
|
+
if (options.visibility) params.set('visibility', options.visibility);
|
|
3528
|
+
if (options.flat) params.set('flat', 'true');
|
|
3529
|
+
const qs = params.toString();
|
|
3530
|
+
const url = `${this.getApiBase()}/skill-groups${qs ? `?${qs}` : ''}`;
|
|
3531
|
+
const response = await fetch(url, {
|
|
3532
|
+
method: 'GET',
|
|
3533
|
+
headers: this.getAuthHeaders()
|
|
3534
|
+
});
|
|
3535
|
+
if (!response.ok) {
|
|
3536
|
+
const data = await response.json();
|
|
3537
|
+
throw new RegistryError(data.error || `Failed to list groups: ${response.status}`, response.status, data);
|
|
3538
|
+
}
|
|
3539
|
+
const body = await response.json();
|
|
3540
|
+
return body.data;
|
|
3541
|
+
}
|
|
3542
|
+
/**
|
|
3543
|
+
* Create a new skill group.
|
|
3544
|
+
*
|
|
3545
|
+
* @param input - Group creation parameters
|
|
3546
|
+
* @returns Created group
|
|
3547
|
+
*/ async createGroup(input) {
|
|
3548
|
+
const url = `${this.getApiBase()}/skill-groups`;
|
|
3549
|
+
const response = await fetch(url, {
|
|
3550
|
+
method: 'POST',
|
|
3551
|
+
headers: {
|
|
3552
|
+
...this.getAuthHeaders(),
|
|
3553
|
+
'Content-Type': 'application/json'
|
|
3554
|
+
},
|
|
3555
|
+
body: JSON.stringify(input)
|
|
3556
|
+
});
|
|
3557
|
+
if (!response.ok) {
|
|
3558
|
+
const data = await response.json();
|
|
3559
|
+
throw new RegistryError(data.error || `Failed to create group: ${response.status}`, response.status, data);
|
|
3560
|
+
}
|
|
3561
|
+
const body = await response.json();
|
|
3562
|
+
return body.data;
|
|
3563
|
+
}
|
|
3564
|
+
/**
|
|
3565
|
+
* Delete a skill group.
|
|
3566
|
+
*
|
|
3567
|
+
* @param groupId - Group UUID
|
|
3568
|
+
* @param dryRun - If true, only preview what would be deleted
|
|
3569
|
+
* @returns Deletion result (affected skills count in dry-run mode)
|
|
3570
|
+
*/ async deleteGroup(groupId, dryRun = false) {
|
|
3571
|
+
const params = dryRun ? '?dry_run=true' : '';
|
|
3572
|
+
const encodedGroupId = encodeURIComponent(groupId);
|
|
3573
|
+
const url = `${this.getApiBase()}/skill-groups/${encodedGroupId}${params}`;
|
|
3574
|
+
const response = await fetch(url, {
|
|
3575
|
+
method: 'DELETE',
|
|
3576
|
+
headers: this.getAuthHeaders()
|
|
3577
|
+
});
|
|
3578
|
+
if (!response.ok) {
|
|
3579
|
+
const data = await response.json();
|
|
3580
|
+
throw new RegistryError(data.error || `Failed to delete group: ${response.status}`, response.status, data);
|
|
3581
|
+
}
|
|
3582
|
+
const body = await response.json();
|
|
3583
|
+
return body.data;
|
|
3584
|
+
}
|
|
3585
|
+
/**
|
|
3586
|
+
* List members of a group.
|
|
3587
|
+
*
|
|
3588
|
+
* @param groupId - Group UUID
|
|
3589
|
+
* @returns Array of members
|
|
3590
|
+
*/ async listGroupMembers(groupId) {
|
|
3591
|
+
const encodedGroupId = encodeURIComponent(groupId);
|
|
3592
|
+
const url = `${this.getApiBase()}/skill-groups/${encodedGroupId}/members`;
|
|
3593
|
+
const response = await fetch(url, {
|
|
3594
|
+
method: 'GET',
|
|
3595
|
+
headers: this.getAuthHeaders()
|
|
3596
|
+
});
|
|
3597
|
+
if (!response.ok) {
|
|
3598
|
+
const data = await response.json();
|
|
3599
|
+
throw new RegistryError(data.error || `Failed to list members: ${response.status}`, response.status, data);
|
|
3600
|
+
}
|
|
3601
|
+
const body = await response.json();
|
|
3602
|
+
return body.data;
|
|
3603
|
+
}
|
|
3604
|
+
/**
|
|
3605
|
+
* Add members to a group.
|
|
3606
|
+
*
|
|
3607
|
+
* @param groupId - Group UUID
|
|
3608
|
+
* @param userIds - Array of user IDs to add
|
|
3609
|
+
* @param role - Role to assign (defaults to 'developer')
|
|
3610
|
+
*/ async addGroupMembers(groupId, userIds, role = 'developer') {
|
|
3611
|
+
const encodedGroupId = encodeURIComponent(groupId);
|
|
3612
|
+
const url = `${this.getApiBase()}/skill-groups/${encodedGroupId}/members`;
|
|
3613
|
+
const response = await fetch(url, {
|
|
3614
|
+
method: 'POST',
|
|
3615
|
+
headers: {
|
|
3616
|
+
...this.getAuthHeaders(),
|
|
3617
|
+
'Content-Type': 'application/json'
|
|
3618
|
+
},
|
|
3619
|
+
body: JSON.stringify({
|
|
3620
|
+
user_ids: userIds,
|
|
3621
|
+
role
|
|
3622
|
+
})
|
|
3623
|
+
});
|
|
3624
|
+
if (!response.ok) {
|
|
3625
|
+
const data = await response.json();
|
|
3626
|
+
throw new RegistryError(data.error || `Failed to add members: ${response.status}`, response.status, data);
|
|
3627
|
+
}
|
|
3628
|
+
}
|
|
3629
|
+
/**
|
|
3630
|
+
* Remove a member from a group.
|
|
3631
|
+
*
|
|
3632
|
+
* @param groupId - Group UUID
|
|
3633
|
+
* @param userId - User ID to remove
|
|
3634
|
+
*/ async removeGroupMember(groupId, userId) {
|
|
3635
|
+
const params = new URLSearchParams({
|
|
3636
|
+
user_id: userId
|
|
3637
|
+
});
|
|
3638
|
+
const encodedGroupId = encodeURIComponent(groupId);
|
|
3639
|
+
const url = `${this.getApiBase()}/skill-groups/${encodedGroupId}/members?${params.toString()}`;
|
|
3640
|
+
const response = await fetch(url, {
|
|
3641
|
+
method: 'DELETE',
|
|
3642
|
+
headers: this.getAuthHeaders()
|
|
3643
|
+
});
|
|
3644
|
+
if (!response.ok) {
|
|
3645
|
+
const data = await response.json();
|
|
3646
|
+
throw new RegistryError(data.error || `Failed to remove member: ${response.status}`, response.status, data);
|
|
3647
|
+
}
|
|
3648
|
+
}
|
|
3649
|
+
/**
|
|
3650
|
+
* Update a member's role in a group.
|
|
3651
|
+
*
|
|
3652
|
+
* @param groupId - Group UUID
|
|
3653
|
+
* @param userId - User ID to update
|
|
3654
|
+
* @param role - New role to assign
|
|
3655
|
+
*/ async updateGroupMemberRole(groupId, userId, role) {
|
|
3656
|
+
const encodedGroupId = encodeURIComponent(groupId);
|
|
3657
|
+
const url = `${this.getApiBase()}/skill-groups/${encodedGroupId}/members`;
|
|
3658
|
+
const response = await fetch(url, {
|
|
3659
|
+
method: 'PATCH',
|
|
3660
|
+
headers: {
|
|
3661
|
+
...this.getAuthHeaders(),
|
|
3662
|
+
'Content-Type': 'application/json'
|
|
3663
|
+
},
|
|
3664
|
+
body: JSON.stringify({
|
|
3665
|
+
user_id: userId,
|
|
3666
|
+
role
|
|
3667
|
+
})
|
|
3668
|
+
});
|
|
3669
|
+
if (!response.ok) {
|
|
3670
|
+
const data = await response.json();
|
|
3671
|
+
throw new RegistryError(data.error || `Failed to update member role: ${response.status}`, response.status, data);
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3493
3674
|
}
|
|
3494
3675
|
/**
|
|
3495
3676
|
* Tarball Extractor (Step 3.6)
|
|
@@ -4784,15 +4965,15 @@ class RegistryResolver {
|
|
|
4784
4965
|
/**
|
|
4785
4966
|
* Install a web-published skill.
|
|
4786
4967
|
*
|
|
4787
|
-
* Web-published skills do not support versioning. Branches to different
|
|
4968
|
+
* Web-published skills (except local) do not support versioning. Branches to different
|
|
4788
4969
|
* installation logic based on source_type:
|
|
4789
4970
|
* - github/gitlab: reuses installToAgentsFromGit
|
|
4790
4971
|
* - oss_url/custom_url: reuses installToAgentsFromHttp
|
|
4791
4972
|
* - local: downloads tarball via Registry API
|
|
4792
4973
|
*/ async installFromWebPublished(skillInfo, parsed, targetAgents, options = {}) {
|
|
4793
4974
|
const { source_type, source_url } = skillInfo;
|
|
4794
|
-
// Web-published skills do not support version specifiers
|
|
4795
|
-
if (parsed.version && 'latest' !== parsed.version) throw new Error(`Version specifier not supported for web-published skills.\n'${parsed.fullName}' was published via web and does not support versioning.\nUse: reskill install ${parsed.fullName}`);
|
|
4975
|
+
// Web-published skills (except local) do not support version specifiers
|
|
4976
|
+
if ('local' !== source_type && parsed.version && 'latest' !== parsed.version) throw new Error(`Version specifier not supported for web-published skills.\n'${parsed.fullName}' was published via web and does not support versioning.\nUse: reskill install ${parsed.fullName}`);
|
|
4796
4977
|
if (!source_url) throw new Error(`Missing source_url for web-published skill: ${parsed.fullName}`);
|
|
4797
4978
|
logger_logger["package"](`Installing ${parsed.fullName} from ${source_type} source...`);
|
|
4798
4979
|
// Build registry context so downstream methods use the registry name
|
|
@@ -4883,12 +5064,13 @@ class RegistryResolver {
|
|
|
4883
5064
|
const { save = true, mode = 'symlink' } = options;
|
|
4884
5065
|
const registryUrl = await this.resolveRegistryUrl(parsed.fullName, options.registry);
|
|
4885
5066
|
const shortName = getShortName(parsed.fullName);
|
|
4886
|
-
|
|
4887
|
-
// Download tarball via RegistryClient (handles auth + 302 redirect to signed URL)
|
|
5067
|
+
// Resolve version via dist-tags (supports @latest, @1.0.0, etc.)
|
|
4888
5068
|
const client = new RegistryClient({
|
|
4889
5069
|
registry: registryUrl,
|
|
4890
5070
|
token: options.token
|
|
4891
5071
|
});
|
|
5072
|
+
const version = await client.resolveVersion(parsed.fullName, parsed.version);
|
|
5073
|
+
// Download tarball via RegistryClient (handles auth + 302 redirect to signed URL)
|
|
4892
5074
|
const { tarball } = await client.downloadSkill(parsed.fullName, version);
|
|
4893
5075
|
logger_logger["package"](`Installing ${shortName} from ${registryUrl} to ${targetAgents.length} agent(s)...`);
|
|
4894
5076
|
// Extract tarball to temp directory (clean stale files first)
|
|
@@ -6420,113 +6602,513 @@ class AuthManager {
|
|
|
6420
6602
|
// ============================================================================
|
|
6421
6603
|
const findCommand = new __WEBPACK_EXTERNAL_MODULE_commander__.Command('find').alias('search').description('Search for skills in the registry').argument('<query>', 'Search query').option('-r, --registry <url>', 'Registry URL (or set RESKILL_REGISTRY env var, or defaults.publishRegistry in skills.json)').option('-l, --limit <n>', 'Maximum number of results', '10').option('-j, --json', 'Output as JSON').action(findAction);
|
|
6422
6604
|
/**
|
|
6423
|
-
*
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
|
|
6605
|
+
* Group path utilities — normalization, slug generation, and validation.
|
|
6606
|
+
*
|
|
6607
|
+
* Shared by the `group` and `publish` CLI commands.
|
|
6608
|
+
*/ const SLUG_REGEX = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
6609
|
+
const MAX_GROUP_DEPTH = 3;
|
|
6610
|
+
const MAX_SEGMENT_LENGTH = 64;
|
|
6611
|
+
/**
|
|
6612
|
+
* Normalize a group path for API usage.
|
|
6613
|
+
*
|
|
6614
|
+
* Rules from spec §13.2:
|
|
6615
|
+
* - Strip leading/trailing slashes and whitespace
|
|
6616
|
+
* - Collapse consecutive slashes
|
|
6617
|
+
* - Lowercase
|
|
6618
|
+
*/ function normalizeGroupPath(raw) {
|
|
6619
|
+
return raw.trim().replace(/\/+/g, '/').replace(/^\/|\/$/g, '').toLowerCase();
|
|
6620
|
+
}
|
|
6621
|
+
/**
|
|
6622
|
+
* Generate a URL-safe slug from a human-readable name.
|
|
6623
|
+
*
|
|
6624
|
+
* Spec §13.4:
|
|
6625
|
+
* - Lowercase, trim, replace spaces/underscores with hyphens
|
|
6626
|
+
* - Strip non-alphanumeric characters (except hyphens)
|
|
6627
|
+
* - Collapse consecutive hyphens, strip leading/trailing hyphens
|
|
6628
|
+
* - Truncate to MAX_SEGMENT_LENGTH characters
|
|
6629
|
+
*/ function generateSlug(name) {
|
|
6630
|
+
return name.toLowerCase().trim().replace(/[\s_]+/g, '-').replace(/[^a-z0-9-]/g, '').replace(/-+/g, '-').replace(/^-|-$/g, '').slice(0, MAX_SEGMENT_LENGTH).replace(/-$/g, '');
|
|
6631
|
+
}
|
|
6632
|
+
/**
|
|
6633
|
+
* Validate a normalized group path.
|
|
6634
|
+
*
|
|
6635
|
+
* Spec §13.2:
|
|
6636
|
+
* - Segment slug must match SLUG_REGEX
|
|
6637
|
+
* - Segment length <= 64
|
|
6638
|
+
* - Max depth <= 3
|
|
6639
|
+
*/ function validateGroupPath(path) {
|
|
6640
|
+
if (!path) return {
|
|
6641
|
+
valid: false,
|
|
6642
|
+
error: 'Group path cannot be empty'
|
|
6643
|
+
};
|
|
6644
|
+
const segments = path.split('/');
|
|
6645
|
+
if (segments.length > MAX_GROUP_DEPTH) return {
|
|
6646
|
+
valid: false,
|
|
6647
|
+
error: `Group path depth cannot exceed ${MAX_GROUP_DEPTH} segments`
|
|
6648
|
+
};
|
|
6649
|
+
for (const segment of segments){
|
|
6650
|
+
if (segment.length > MAX_SEGMENT_LENGTH) return {
|
|
6651
|
+
valid: false,
|
|
6652
|
+
error: `Group path segment "${segment}" exceeds ${MAX_SEGMENT_LENGTH} characters`
|
|
6653
|
+
};
|
|
6654
|
+
if (!SLUG_REGEX.test(segment)) return {
|
|
6655
|
+
valid: false,
|
|
6656
|
+
error: `Invalid group path segment "${segment}". Segments must match ${SLUG_REGEX}`
|
|
6657
|
+
};
|
|
6430
6658
|
}
|
|
6431
|
-
|
|
6432
|
-
|
|
6659
|
+
return {
|
|
6660
|
+
valid: true
|
|
6661
|
+
};
|
|
6662
|
+
}
|
|
6663
|
+
/**
|
|
6664
|
+
* group command - Manage skill groups
|
|
6665
|
+
*
|
|
6666
|
+
* Provides subcommands for listing, creating, inspecting, and deleting
|
|
6667
|
+
* skill groups, as well as managing group membership.
|
|
6668
|
+
*
|
|
6669
|
+
* Usage:
|
|
6670
|
+
* reskill group list # List visible groups
|
|
6671
|
+
* reskill group create <name> # Create a group
|
|
6672
|
+
* reskill group info <path> # Show group details
|
|
6673
|
+
* reskill group delete <path> # Delete a group
|
|
6674
|
+
* reskill group member list <path> # List members
|
|
6675
|
+
* reskill group member add <path> <users...> # Add members
|
|
6676
|
+
* reskill group member remove <path> <user> # Remove a member
|
|
6677
|
+
* reskill group member role <path> <user> <role> # Change member role
|
|
6678
|
+
*/ // ============================================================================
|
|
6679
|
+
// Constants
|
|
6680
|
+
// ============================================================================
|
|
6681
|
+
const VALID_ROLES = [
|
|
6682
|
+
'owner',
|
|
6683
|
+
'maintainer',
|
|
6684
|
+
'developer'
|
|
6685
|
+
];
|
|
6686
|
+
/**
|
|
6687
|
+
* Validate that a role string is one of the allowed values.
|
|
6688
|
+
*/ function validateRole(role) {
|
|
6689
|
+
return VALID_ROLES.includes(role);
|
|
6690
|
+
}
|
|
6691
|
+
function assertValidGroupPath(path) {
|
|
6692
|
+
const validation = validateGroupPath(path);
|
|
6693
|
+
if (!validation.valid) {
|
|
6694
|
+
logger_logger.error(validation.error);
|
|
6433
6695
|
process.exit(1);
|
|
6434
6696
|
}
|
|
6435
|
-
|
|
6436
|
-
logger_logger.newline();
|
|
6437
|
-
if (info.config) {
|
|
6438
|
-
logger_logger.log("Configuration (skills.json):");
|
|
6439
|
-
logger_logger.log(` Reference: ${info.config}`);
|
|
6440
|
-
}
|
|
6441
|
-
if (info.locked) {
|
|
6442
|
-
logger_logger.log("Locked Version (skills.lock):");
|
|
6443
|
-
logger_logger.log(` Version: ${info.locked.version}`);
|
|
6444
|
-
logger_logger.log(` Source: ${info.locked.source}`);
|
|
6445
|
-
logger_logger.log(` Commit: ${info.locked.commit}`);
|
|
6446
|
-
logger_logger.log(` Installed: ${info.locked.installedAt}`);
|
|
6447
|
-
}
|
|
6448
|
-
if (info.installed) {
|
|
6449
|
-
logger_logger.log("Installed:");
|
|
6450
|
-
logger_logger.log(` Path: ${info.installed.path}`);
|
|
6451
|
-
logger_logger.log(` Version: ${info.installed.version}`);
|
|
6452
|
-
logger_logger.log(` Linked: ${info.installed.isLinked ? 'Yes' : 'No'}`);
|
|
6453
|
-
if (info.installed.metadata) {
|
|
6454
|
-
const meta = info.installed.metadata;
|
|
6455
|
-
logger_logger.log("Metadata (SKILL.md):");
|
|
6456
|
-
if (meta.description) logger_logger.log(` Description: ${meta.description}`);
|
|
6457
|
-
if (meta.author) logger_logger.log(` Author: ${meta.author}`);
|
|
6458
|
-
if (meta.license) logger_logger.log(` License: ${meta.license}`);
|
|
6459
|
-
if (meta.keywords?.length) logger_logger.log(` Keywords: ${meta.keywords.join(', ')}`);
|
|
6460
|
-
}
|
|
6461
|
-
} else logger_logger.warn(`Skill ${skillName} is not installed`);
|
|
6462
|
-
});
|
|
6697
|
+
}
|
|
6463
6698
|
// ============================================================================
|
|
6464
|
-
//
|
|
6699
|
+
// Client Factory
|
|
6465
6700
|
// ============================================================================
|
|
6466
|
-
|
|
6701
|
+
function createClient(registry) {
|
|
6702
|
+
const authManager = new AuthManager();
|
|
6703
|
+
const token = authManager.getToken(registry);
|
|
6704
|
+
if (!token) {
|
|
6705
|
+
logger_logger.error('Authentication required');
|
|
6706
|
+
logger_logger.newline();
|
|
6707
|
+
logger_logger.log("Run 'reskill login' to authenticate.");
|
|
6708
|
+
process.exit(1);
|
|
6709
|
+
}
|
|
6710
|
+
return new RegistryClient({
|
|
6711
|
+
registry,
|
|
6712
|
+
token
|
|
6713
|
+
});
|
|
6714
|
+
}
|
|
6467
6715
|
// ============================================================================
|
|
6468
|
-
//
|
|
6716
|
+
// Display Helpers
|
|
6469
6717
|
// ============================================================================
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
6473
|
-
|
|
6718
|
+
function getGroupIndentLevel(group) {
|
|
6719
|
+
if ('number' == typeof group.level && Number.isFinite(group.level)) return Math.max(0, group.level - 1);
|
|
6720
|
+
return Math.max(0, group.path.split('/').length - 1);
|
|
6721
|
+
}
|
|
6722
|
+
function displayGroupList(groups, json, tree = false) {
|
|
6723
|
+
if (json) {
|
|
6724
|
+
console.log(JSON.stringify(groups, null, 2));
|
|
6725
|
+
return;
|
|
6726
|
+
}
|
|
6727
|
+
if (0 === groups.length) {
|
|
6728
|
+
logger_logger.warn('No groups found');
|
|
6729
|
+
return;
|
|
6730
|
+
}
|
|
6474
6731
|
logger_logger.newline();
|
|
6475
|
-
logger_logger.log('
|
|
6476
|
-
logger_logger.
|
|
6732
|
+
logger_logger.log(`Found ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].bold(String(groups.length))} group${1 === groups.length ? '' : 's'}:`);
|
|
6733
|
+
logger_logger.newline();
|
|
6734
|
+
if (tree) {
|
|
6735
|
+
const sortedGroups = [
|
|
6736
|
+
...groups
|
|
6737
|
+
].sort((a, b)=>a.path.localeCompare(b.path));
|
|
6738
|
+
for (const group of sortedGroups){
|
|
6739
|
+
const vis = 'private' === group.visibility ? __WEBPACK_EXTERNAL_MODULE_chalk__["default"].yellow(' (private)') : '';
|
|
6740
|
+
const desc = group.description ? __WEBPACK_EXTERNAL_MODULE_chalk__["default"].gray(` - ${group.description}`) : '';
|
|
6741
|
+
const indent = ' '.repeat(getGroupIndentLevel(group));
|
|
6742
|
+
const nodeLabel = group.path.split('/').pop() || group.path;
|
|
6743
|
+
logger_logger.log(` ${indent}└─ ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].bold.cyan(nodeLabel)}${vis}${desc} ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].gray(`(${group.path})`)}`);
|
|
6744
|
+
}
|
|
6745
|
+
logger_logger.newline();
|
|
6746
|
+
return;
|
|
6747
|
+
}
|
|
6748
|
+
for (const group of groups){
|
|
6749
|
+
const vis = 'private' === group.visibility ? __WEBPACK_EXTERNAL_MODULE_chalk__["default"].yellow(' (private)') : '';
|
|
6750
|
+
const desc = group.description ? __WEBPACK_EXTERNAL_MODULE_chalk__["default"].gray(` - ${group.description}`) : '';
|
|
6751
|
+
logger_logger.log(` ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].bold.cyan(group.path)}${vis}${desc}`);
|
|
6752
|
+
const meta = [];
|
|
6753
|
+
if (void 0 !== group.skill_count) meta.push(`${group.skill_count} skill(s)`);
|
|
6754
|
+
if (void 0 !== group.member_count) meta.push(`${group.member_count} member(s)`);
|
|
6755
|
+
if (meta.length > 0) logger_logger.log(` ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].gray(meta.join(' · '))}`);
|
|
6756
|
+
}
|
|
6477
6757
|
logger_logger.newline();
|
|
6478
|
-
logger_logger.log('Next steps:');
|
|
6479
|
-
logger_logger.log(' reskill install <skill> Install a skill');
|
|
6480
|
-
logger_logger.log(' reskill list List installed skills');
|
|
6481
6758
|
}
|
|
6482
|
-
|
|
6483
|
-
|
|
6484
|
-
|
|
6485
|
-
/**
|
|
6486
|
-
* init command - Initialize skills.json configuration
|
|
6487
|
-
*
|
|
6488
|
-
* Creates a new skills.json file in the current directory with default settings.
|
|
6489
|
-
* Will not overwrite an existing skills.json file.
|
|
6490
|
-
*
|
|
6491
|
-
* @example
|
|
6492
|
-
* ```bash
|
|
6493
|
-
* # Initialize with defaults
|
|
6494
|
-
* reskill init
|
|
6495
|
-
*
|
|
6496
|
-
* # Initialize with custom install directory
|
|
6497
|
-
* reskill init -d my-skills
|
|
6498
|
-
*
|
|
6499
|
-
* # Skip prompts (for CI/scripts)
|
|
6500
|
-
* reskill init -y
|
|
6501
|
-
* ```
|
|
6502
|
-
*/ const initCommand = new __WEBPACK_EXTERNAL_MODULE_commander__.Command('init').description('Initialize a new skills.json configuration').option('-d, --install-dir <dir>', 'Skills installation directory', DEFAULT_INSTALL_DIR).option('-y, --yes', 'Skip prompts and use defaults').action((options)=>{
|
|
6503
|
-
const configLoader = new ConfigLoader();
|
|
6504
|
-
// Check if configuration already exists
|
|
6505
|
-
if (configLoader.exists()) {
|
|
6506
|
-
logger_logger.warn('skills.json already exists');
|
|
6759
|
+
function displayGroupDetail(detail, json) {
|
|
6760
|
+
if (json) {
|
|
6761
|
+
console.log(JSON.stringify(detail, null, 2));
|
|
6507
6762
|
return;
|
|
6508
6763
|
}
|
|
6509
|
-
|
|
6510
|
-
|
|
6511
|
-
|
|
6512
|
-
|
|
6513
|
-
|
|
6514
|
-
});
|
|
6515
|
-
|
|
6516
|
-
|
|
6517
|
-
});
|
|
6764
|
+
logger_logger.newline();
|
|
6765
|
+
logger_logger.log(`${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].bold(detail.name)} ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].gray(`(${detail.path})`)}`);
|
|
6766
|
+
if (detail.description) logger_logger.log(` ${detail.description}`);
|
|
6767
|
+
logger_logger.newline();
|
|
6768
|
+
const vis = 'private' === detail.visibility ? __WEBPACK_EXTERNAL_MODULE_chalk__["default"].yellow('private') : __WEBPACK_EXTERNAL_MODULE_chalk__["default"].green('public');
|
|
6769
|
+
logger_logger.log(` Visibility: ${vis}`);
|
|
6770
|
+
logger_logger.log(` Level: ${detail.level}`);
|
|
6771
|
+
if (void 0 !== detail.skill_count) logger_logger.log(` Skills: ${detail.skill_count}`);
|
|
6772
|
+
if (void 0 !== detail.member_count) logger_logger.log(` Members: ${detail.member_count}`);
|
|
6773
|
+
if (detail.current_user_role) logger_logger.log(` Your role: ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].bold(detail.current_user_role)}`);
|
|
6774
|
+
if (detail.children && detail.children.length > 0) {
|
|
6775
|
+
logger_logger.newline();
|
|
6776
|
+
logger_logger.log(' Sub groups:');
|
|
6777
|
+
for (const child of detail.children)logger_logger.log(` • ${child.path}`);
|
|
6778
|
+
}
|
|
6779
|
+
logger_logger.newline();
|
|
6780
|
+
}
|
|
6781
|
+
function displayMemberList(members, groupPath, json) {
|
|
6782
|
+
if (json) {
|
|
6783
|
+
console.log(JSON.stringify(members, null, 2));
|
|
6784
|
+
return;
|
|
6785
|
+
}
|
|
6786
|
+
if (0 === members.length) {
|
|
6787
|
+
logger_logger.warn(`No members in group "${groupPath}"`);
|
|
6788
|
+
return;
|
|
6789
|
+
}
|
|
6790
|
+
logger_logger.newline();
|
|
6791
|
+
logger_logger.log(`${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].bold(String(members.length))} member${1 === members.length ? '' : 's'} in ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].bold(groupPath)}:`);
|
|
6792
|
+
logger_logger.newline();
|
|
6793
|
+
for (const member of members){
|
|
6794
|
+
const role = __WEBPACK_EXTERNAL_MODULE_chalk__["default"].bold(member.role);
|
|
6795
|
+
const handle = member.handle || member.user_id;
|
|
6796
|
+
logger_logger.log(` ${handle} ${role}`);
|
|
6797
|
+
}
|
|
6798
|
+
logger_logger.newline();
|
|
6799
|
+
}
|
|
6518
6800
|
// ============================================================================
|
|
6519
|
-
//
|
|
6801
|
+
// Subcommand Actions
|
|
6520
6802
|
// ============================================================================
|
|
6521
|
-
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
|
|
6528
|
-
|
|
6529
|
-
|
|
6803
|
+
async function listAction(options) {
|
|
6804
|
+
const registry = resolveRegistry(options.registry);
|
|
6805
|
+
const client = createClient(registry);
|
|
6806
|
+
try {
|
|
6807
|
+
const groups = await client.listGroups({
|
|
6808
|
+
flat: Boolean(options.tree)
|
|
6809
|
+
});
|
|
6810
|
+
displayGroupList(groups, options.json || false, Boolean(options.tree));
|
|
6811
|
+
} catch (error) {
|
|
6812
|
+
logger_logger.error(`Failed to list groups: ${error.message}`);
|
|
6813
|
+
process.exit(1);
|
|
6814
|
+
}
|
|
6815
|
+
}
|
|
6816
|
+
async function createAction(name, options) {
|
|
6817
|
+
const registry = resolveRegistry(options.registry);
|
|
6818
|
+
const slug = generateSlug(name);
|
|
6819
|
+
if (!slug) {
|
|
6820
|
+
logger_logger.error('Name must contain at least one ASCII alphanumeric character to generate a valid slug');
|
|
6821
|
+
process.exit(1);
|
|
6822
|
+
}
|
|
6823
|
+
if (!SLUG_REGEX.test(slug)) {
|
|
6824
|
+
logger_logger.error(`Generated slug "${slug}" is invalid. Name must produce a slug matching ${SLUG_REGEX.source}`);
|
|
6825
|
+
process.exit(1);
|
|
6826
|
+
}
|
|
6827
|
+
let parentId;
|
|
6828
|
+
let client;
|
|
6829
|
+
if (options.parent) {
|
|
6830
|
+
const normalizedParent = normalizeGroupPath(options.parent);
|
|
6831
|
+
assertValidGroupPath(normalizedParent);
|
|
6832
|
+
client = createClient(registry);
|
|
6833
|
+
try {
|
|
6834
|
+
const parentGroup = await client.resolveGroup(normalizedParent);
|
|
6835
|
+
parentId = parentGroup.id;
|
|
6836
|
+
} catch (error) {
|
|
6837
|
+
if (error instanceof RegistryError) logger_logger.error(`Parent group "${options.parent}" not found`);
|
|
6838
|
+
else logger_logger.error(`Failed to resolve parent: ${error.message}`);
|
|
6839
|
+
process.exit(1);
|
|
6840
|
+
}
|
|
6841
|
+
}
|
|
6842
|
+
try {
|
|
6843
|
+
const ensuredClient = client ?? createClient(registry);
|
|
6844
|
+
const group = await ensuredClient.createGroup({
|
|
6845
|
+
name,
|
|
6846
|
+
slug,
|
|
6847
|
+
description: options.description,
|
|
6848
|
+
visibility: options.visibility,
|
|
6849
|
+
parent_id: parentId
|
|
6850
|
+
});
|
|
6851
|
+
if (options.json) console.log(JSON.stringify(group, null, 2));
|
|
6852
|
+
else {
|
|
6853
|
+
logger_logger.newline();
|
|
6854
|
+
logger_logger.success(`Group created: ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].bold(group.path)}`);
|
|
6855
|
+
logger_logger.log(` ID: ${group.id}`);
|
|
6856
|
+
logger_logger.log(` Visibility: ${group.visibility}`);
|
|
6857
|
+
logger_logger.newline();
|
|
6858
|
+
}
|
|
6859
|
+
} catch (error) {
|
|
6860
|
+
logger_logger.error(`Failed to create group: ${error.message}`);
|
|
6861
|
+
process.exit(1);
|
|
6862
|
+
}
|
|
6863
|
+
}
|
|
6864
|
+
async function infoAction(groupPath, options) {
|
|
6865
|
+
const normalized = normalizeGroupPath(groupPath);
|
|
6866
|
+
assertValidGroupPath(normalized);
|
|
6867
|
+
const registry = resolveRegistry(options.registry);
|
|
6868
|
+
const client = createClient(registry);
|
|
6869
|
+
try {
|
|
6870
|
+
const detail = await client.resolveGroup(normalized);
|
|
6871
|
+
displayGroupDetail(detail, options.json || false);
|
|
6872
|
+
} catch (error) {
|
|
6873
|
+
if (error instanceof RegistryError) {
|
|
6874
|
+
if (404 === error.statusCode) logger_logger.error(`Group "${groupPath}" not found`);
|
|
6875
|
+
else logger_logger.error(`Failed to get group info: ${error.message}`);
|
|
6876
|
+
} else logger_logger.error(`Failed to get group info: ${error.message}`);
|
|
6877
|
+
process.exit(1);
|
|
6878
|
+
}
|
|
6879
|
+
}
|
|
6880
|
+
async function deleteAction(groupPath, options) {
|
|
6881
|
+
const normalized = normalizeGroupPath(groupPath);
|
|
6882
|
+
assertValidGroupPath(normalized);
|
|
6883
|
+
const registry = resolveRegistry(options.registry);
|
|
6884
|
+
const client = createClient(registry);
|
|
6885
|
+
try {
|
|
6886
|
+
const detail = await client.resolveGroup(normalized);
|
|
6887
|
+
if (options.dryRun) {
|
|
6888
|
+
const result = await client.deleteGroup(detail.id, true);
|
|
6889
|
+
logger_logger.newline();
|
|
6890
|
+
logger_logger.log(`Dry run: deleting group "${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].bold(groupPath)}"`);
|
|
6891
|
+
if (void 0 !== result.affected_skills) logger_logger.log(` Affected skills: ${result.affected_skills}`);
|
|
6892
|
+
logger_logger.log('No changes made (--dry-run)');
|
|
6893
|
+
logger_logger.newline();
|
|
6894
|
+
return;
|
|
6895
|
+
}
|
|
6896
|
+
if (!options.yes) {
|
|
6897
|
+
const { createInterface } = await import("node:readline");
|
|
6898
|
+
const rl = createInterface({
|
|
6899
|
+
input: process.stdin,
|
|
6900
|
+
output: process.stdout
|
|
6901
|
+
});
|
|
6902
|
+
const answer = await new Promise((resolve)=>{
|
|
6903
|
+
rl.question(`\n? Delete group "${groupPath}" and all its contents? (y/N) `, resolve);
|
|
6904
|
+
rl.once('close', ()=>resolve(''));
|
|
6905
|
+
});
|
|
6906
|
+
rl.close();
|
|
6907
|
+
if ('y' !== answer.trim().toLowerCase() && 'yes' !== answer.trim().toLowerCase()) {
|
|
6908
|
+
logger_logger.log('Cancelled.');
|
|
6909
|
+
return;
|
|
6910
|
+
}
|
|
6911
|
+
}
|
|
6912
|
+
await client.deleteGroup(detail.id, false);
|
|
6913
|
+
logger_logger.success(`Group "${groupPath}" deleted`);
|
|
6914
|
+
} catch (error) {
|
|
6915
|
+
if (error instanceof RegistryError) {
|
|
6916
|
+
if (404 === error.statusCode) logger_logger.error(`Group "${groupPath}" not found`);
|
|
6917
|
+
else logger_logger.error(`Failed to delete group: ${error.message}`);
|
|
6918
|
+
} else logger_logger.error(`Failed to delete group: ${error.message}`);
|
|
6919
|
+
process.exit(1);
|
|
6920
|
+
}
|
|
6921
|
+
}
|
|
6922
|
+
// ============================================================================
|
|
6923
|
+
// Member Subcommand Actions
|
|
6924
|
+
// ============================================================================
|
|
6925
|
+
async function memberListAction(groupPath, options) {
|
|
6926
|
+
const normalized = normalizeGroupPath(groupPath);
|
|
6927
|
+
assertValidGroupPath(normalized);
|
|
6928
|
+
const registry = resolveRegistry(options.registry);
|
|
6929
|
+
const client = createClient(registry);
|
|
6930
|
+
try {
|
|
6931
|
+
const detail = await client.resolveGroup(normalized);
|
|
6932
|
+
const members = await client.listGroupMembers(detail.id);
|
|
6933
|
+
displayMemberList(members, groupPath, options.json || false);
|
|
6934
|
+
} catch (error) {
|
|
6935
|
+
logger_logger.error(`Failed to list members: ${error.message}`);
|
|
6936
|
+
process.exit(1);
|
|
6937
|
+
}
|
|
6938
|
+
}
|
|
6939
|
+
async function memberAddAction(groupPath, userIds, options) {
|
|
6940
|
+
const normalized = normalizeGroupPath(groupPath);
|
|
6941
|
+
assertValidGroupPath(normalized);
|
|
6942
|
+
const registry = resolveRegistry(options.registry);
|
|
6943
|
+
const client = createClient(registry);
|
|
6944
|
+
const role = options.role || 'developer';
|
|
6945
|
+
if (!validateRole(role)) {
|
|
6946
|
+
logger_logger.error(`Invalid role "${role}". Must be one of: ${VALID_ROLES.join(', ')}`);
|
|
6947
|
+
process.exit(1);
|
|
6948
|
+
}
|
|
6949
|
+
try {
|
|
6950
|
+
const detail = await client.resolveGroup(normalized);
|
|
6951
|
+
await client.addGroupMembers(detail.id, userIds, role);
|
|
6952
|
+
logger_logger.success(`Added ${userIds.length} member(s) to "${groupPath}" as ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].bold(role)}`);
|
|
6953
|
+
} catch (error) {
|
|
6954
|
+
logger_logger.error(`Failed to add members: ${error.message}`);
|
|
6955
|
+
process.exit(1);
|
|
6956
|
+
}
|
|
6957
|
+
}
|
|
6958
|
+
async function memberRemoveAction(groupPath, userId, options) {
|
|
6959
|
+
const normalized = normalizeGroupPath(groupPath);
|
|
6960
|
+
assertValidGroupPath(normalized);
|
|
6961
|
+
const registry = resolveRegistry(options.registry);
|
|
6962
|
+
const client = createClient(registry);
|
|
6963
|
+
try {
|
|
6964
|
+
const detail = await client.resolveGroup(normalized);
|
|
6965
|
+
await client.removeGroupMember(detail.id, userId);
|
|
6966
|
+
logger_logger.success(`Removed "${userId}" from "${groupPath}"`);
|
|
6967
|
+
} catch (error) {
|
|
6968
|
+
logger_logger.error(`Failed to remove member: ${error.message}`);
|
|
6969
|
+
process.exit(1);
|
|
6970
|
+
}
|
|
6971
|
+
}
|
|
6972
|
+
async function memberRoleAction(groupPath, userId, role, options) {
|
|
6973
|
+
const normalized = normalizeGroupPath(groupPath);
|
|
6974
|
+
assertValidGroupPath(normalized);
|
|
6975
|
+
const registry = resolveRegistry(options.registry);
|
|
6976
|
+
const client = createClient(registry);
|
|
6977
|
+
if (!validateRole(role)) {
|
|
6978
|
+
logger_logger.error(`Invalid role "${role}". Must be one of: ${VALID_ROLES.join(', ')}`);
|
|
6979
|
+
process.exit(1);
|
|
6980
|
+
}
|
|
6981
|
+
try {
|
|
6982
|
+
const detail = await client.resolveGroup(normalized);
|
|
6983
|
+
await client.updateGroupMemberRole(detail.id, userId, role);
|
|
6984
|
+
logger_logger.success(`Updated role of "${userId}" in "${groupPath}" to ${__WEBPACK_EXTERNAL_MODULE_chalk__["default"].bold(role)}`);
|
|
6985
|
+
} catch (error) {
|
|
6986
|
+
logger_logger.error(`Failed to update role: ${error.message}`);
|
|
6987
|
+
process.exit(1);
|
|
6988
|
+
}
|
|
6989
|
+
}
|
|
6990
|
+
// ============================================================================
|
|
6991
|
+
// Command Definitions
|
|
6992
|
+
// ============================================================================
|
|
6993
|
+
const memberCommand = new __WEBPACK_EXTERNAL_MODULE_commander__.Command('member').description('Manage group members');
|
|
6994
|
+
memberCommand.command('list <path>').description('List members of a group').option('-r, --registry <url>', 'Registry URL').option('-j, --json', 'Output as JSON').action(memberListAction);
|
|
6995
|
+
memberCommand.command('add <path> <users...>').description('Add members to a group').option('-r, --registry <url>', 'Registry URL').option('--role <role>', 'Role to assign (owner|maintainer|developer)', 'developer').action(memberAddAction);
|
|
6996
|
+
memberCommand.command('remove <path> <user>').description('Remove a member from a group').option('-r, --registry <url>', 'Registry URL').action(memberRemoveAction);
|
|
6997
|
+
memberCommand.command('role <path> <user> <role>').description("Change a member's role").option('-r, --registry <url>', 'Registry URL').action(memberRoleAction);
|
|
6998
|
+
const groupCommand = new __WEBPACK_EXTERNAL_MODULE_commander__.Command('group').description('Manage skill groups');
|
|
6999
|
+
groupCommand.command('list').description('List visible groups').option('-r, --registry <url>', 'Registry URL').option('--tree', 'Render groups as a tree (requests flat group list)').option('-j, --json', 'Output as JSON').action(listAction);
|
|
7000
|
+
groupCommand.command('create <name>').description('Create a new group').option('-r, --registry <url>', 'Registry URL').option('-d, --description <text>', 'Group description').option('--visibility <level>', 'Visibility: public or private', 'public').option('--parent <path>', 'Parent group path (for sub groups)').option('-j, --json', 'Output as JSON').action(createAction);
|
|
7001
|
+
groupCommand.command('info <path>').description('Show group details').option('-r, --registry <url>', 'Registry URL').option('-j, --json', 'Output as JSON').action(infoAction);
|
|
7002
|
+
groupCommand.command('delete <path>').description('Delete a group').option('-r, --registry <url>', 'Registry URL').option('-n, --dry-run', 'Preview deletion without executing').option('-y, --yes', 'Skip confirmation').action(deleteAction);
|
|
7003
|
+
groupCommand.addCommand(memberCommand);
|
|
7004
|
+
/**
|
|
7005
|
+
* info command - Show skill details
|
|
7006
|
+
*/ const infoCommand = new __WEBPACK_EXTERNAL_MODULE_commander__.Command('info').description('Show skill details').argument('<skill>', 'Skill name').option('-j, --json', 'Output as JSON').action((skillName, options)=>{
|
|
7007
|
+
const skillManager = new SkillManager();
|
|
7008
|
+
const info = skillManager.getInfo(skillName);
|
|
7009
|
+
if (options.json) {
|
|
7010
|
+
console.log(JSON.stringify(info, null, 2));
|
|
7011
|
+
return;
|
|
7012
|
+
}
|
|
7013
|
+
if (!info.installed && !info.config) {
|
|
7014
|
+
logger_logger.error(`Skill ${skillName} not found`);
|
|
7015
|
+
process.exit(1);
|
|
7016
|
+
}
|
|
7017
|
+
logger_logger.log(`Skill: ${skillName}`);
|
|
7018
|
+
logger_logger.newline();
|
|
7019
|
+
if (info.config) {
|
|
7020
|
+
logger_logger.log("Configuration (skills.json):");
|
|
7021
|
+
logger_logger.log(` Reference: ${info.config}`);
|
|
7022
|
+
}
|
|
7023
|
+
if (info.locked) {
|
|
7024
|
+
logger_logger.log("Locked Version (skills.lock):");
|
|
7025
|
+
logger_logger.log(` Version: ${info.locked.version}`);
|
|
7026
|
+
logger_logger.log(` Source: ${info.locked.source}`);
|
|
7027
|
+
logger_logger.log(` Commit: ${info.locked.commit}`);
|
|
7028
|
+
logger_logger.log(` Installed: ${info.locked.installedAt}`);
|
|
7029
|
+
}
|
|
7030
|
+
if (info.installed) {
|
|
7031
|
+
logger_logger.log("Installed:");
|
|
7032
|
+
logger_logger.log(` Path: ${info.installed.path}`);
|
|
7033
|
+
logger_logger.log(` Version: ${info.installed.version}`);
|
|
7034
|
+
logger_logger.log(` Linked: ${info.installed.isLinked ? 'Yes' : 'No'}`);
|
|
7035
|
+
if (info.installed.metadata) {
|
|
7036
|
+
const meta = info.installed.metadata;
|
|
7037
|
+
logger_logger.log("Metadata (SKILL.md):");
|
|
7038
|
+
if (meta.description) logger_logger.log(` Description: ${meta.description}`);
|
|
7039
|
+
if (meta.author) logger_logger.log(` Author: ${meta.author}`);
|
|
7040
|
+
if (meta.license) logger_logger.log(` License: ${meta.license}`);
|
|
7041
|
+
if (meta.keywords?.length) logger_logger.log(` Keywords: ${meta.keywords.join(', ')}`);
|
|
7042
|
+
}
|
|
7043
|
+
} else logger_logger.warn(`Skill ${skillName} is not installed`);
|
|
7044
|
+
});
|
|
7045
|
+
// ============================================================================
|
|
7046
|
+
// Constants
|
|
7047
|
+
// ============================================================================
|
|
7048
|
+
const DEFAULT_INSTALL_DIR = '.skills';
|
|
7049
|
+
// ============================================================================
|
|
7050
|
+
// Helper Functions
|
|
7051
|
+
// ============================================================================
|
|
7052
|
+
/**
|
|
7053
|
+
* Display configuration summary after initialization
|
|
7054
|
+
*/ function displayConfigSummary(installDir) {
|
|
7055
|
+
logger_logger.success('Created skills.json');
|
|
7056
|
+
logger_logger.newline();
|
|
7057
|
+
logger_logger.log('Configuration:');
|
|
7058
|
+
logger_logger.log(` Install directory: ${installDir}`);
|
|
7059
|
+
logger_logger.newline();
|
|
7060
|
+
logger_logger.log('Next steps:');
|
|
7061
|
+
logger_logger.log(' reskill install <skill> Install a skill');
|
|
7062
|
+
logger_logger.log(' reskill list List installed skills');
|
|
7063
|
+
}
|
|
7064
|
+
// ============================================================================
|
|
7065
|
+
// Command Definition
|
|
7066
|
+
// ============================================================================
|
|
7067
|
+
/**
|
|
7068
|
+
* init command - Initialize skills.json configuration
|
|
7069
|
+
*
|
|
7070
|
+
* Creates a new skills.json file in the current directory with default settings.
|
|
7071
|
+
* Will not overwrite an existing skills.json file.
|
|
7072
|
+
*
|
|
7073
|
+
* @example
|
|
7074
|
+
* ```bash
|
|
7075
|
+
* # Initialize with defaults
|
|
7076
|
+
* reskill init
|
|
7077
|
+
*
|
|
7078
|
+
* # Initialize with custom install directory
|
|
7079
|
+
* reskill init -d my-skills
|
|
7080
|
+
*
|
|
7081
|
+
* # Skip prompts (for CI/scripts)
|
|
7082
|
+
* reskill init -y
|
|
7083
|
+
* ```
|
|
7084
|
+
*/ const initCommand = new __WEBPACK_EXTERNAL_MODULE_commander__.Command('init').description('Initialize a new skills.json configuration').option('-d, --install-dir <dir>', 'Skills installation directory', DEFAULT_INSTALL_DIR).option('-y, --yes', 'Skip prompts and use defaults').action((options)=>{
|
|
7085
|
+
const configLoader = new ConfigLoader();
|
|
7086
|
+
// Check if configuration already exists
|
|
7087
|
+
if (configLoader.exists()) {
|
|
7088
|
+
logger_logger.warn('skills.json already exists');
|
|
7089
|
+
return;
|
|
7090
|
+
}
|
|
7091
|
+
// Create new configuration
|
|
7092
|
+
configLoader.create({
|
|
7093
|
+
defaults: {
|
|
7094
|
+
installDir: options.installDir
|
|
7095
|
+
}
|
|
7096
|
+
});
|
|
7097
|
+
// Display summary (use options.installDir directly since we just set it)
|
|
7098
|
+
displayConfigSummary(options.installDir);
|
|
7099
|
+
});
|
|
7100
|
+
// ============================================================================
|
|
7101
|
+
// Utility Functions
|
|
7102
|
+
// ============================================================================
|
|
7103
|
+
/**
|
|
7104
|
+
* Format agent names list for display
|
|
7105
|
+
* Truncates long lists with "+N more" suffix
|
|
7106
|
+
*/ function formatAgentNames(agentTypes, maxShow = 5) {
|
|
7107
|
+
const names = agentTypes.map((a)=>agents[a].displayName);
|
|
7108
|
+
if (names.length <= maxShow) return names.join(', ');
|
|
7109
|
+
const shown = names.slice(0, maxShow);
|
|
7110
|
+
const remaining = names.length - maxShow;
|
|
7111
|
+
return `${shown.join(', ')} +${remaining} more`;
|
|
6530
7112
|
}
|
|
6531
7113
|
/**
|
|
6532
7114
|
* Format agent names with chalk coloring
|
|
@@ -7483,955 +8065,956 @@ const logoutCommand = new __WEBPACK_EXTERNAL_MODULE_commander__.Command('logout'
|
|
|
7483
8065
|
}
|
|
7484
8066
|
});
|
|
7485
8067
|
/**
|
|
7486
|
-
*
|
|
8068
|
+
* ContentScanner - Detect malicious patterns in SKILL.md content
|
|
7487
8069
|
*
|
|
7488
|
-
*
|
|
7489
|
-
|
|
7490
|
-
|
|
7491
|
-
|
|
7492
|
-
|
|
7493
|
-
|
|
7494
|
-
|
|
7495
|
-
//
|
|
7496
|
-
// Publisher Class
|
|
8070
|
+
* Features:
|
|
8071
|
+
* - Context-aware: skips safe zones (frontmatter, code blocks, quotes, blockquotes)
|
|
8072
|
+
* - 6 built-in detection rules across 3 risk levels
|
|
8073
|
+
* - Configurable: override levels, disable rules, add custom rules
|
|
8074
|
+
* - Pure string operations in scan() — no fs dependency, suitable for server use
|
|
8075
|
+
* - scanFile() convenience method for CLI use
|
|
8076
|
+
*/ // ============================================================================
|
|
8077
|
+
// Safe Zone Masking
|
|
7497
8078
|
// ============================================================================
|
|
7498
|
-
|
|
7499
|
-
|
|
7500
|
-
|
|
7501
|
-
|
|
7502
|
-
|
|
7503
|
-
|
|
7504
|
-
|
|
7505
|
-
|
|
7506
|
-
|
|
7507
|
-
|
|
7508
|
-
|
|
7509
|
-
|
|
7510
|
-
|
|
7511
|
-
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
|
|
7524
|
-
|
|
7525
|
-
|
|
7526
|
-
cwd: skillPath,
|
|
7527
|
-
encoding: 'utf-8'
|
|
7528
|
-
}).trim();
|
|
7529
|
-
// Parse to sourceRef format
|
|
7530
|
-
info.sourceRef = this.parseRemoteToSourceRef(info.remoteUrl);
|
|
7531
|
-
} catch {
|
|
7532
|
-
// No remote configured
|
|
7533
|
-
}
|
|
7534
|
-
// Get current branch
|
|
7535
|
-
try {
|
|
7536
|
-
info.currentBranch = (0, __WEBPACK_EXTERNAL_MODULE_node_child_process__.execSync)('git branch --show-current', {
|
|
7537
|
-
cwd: skillPath,
|
|
7538
|
-
encoding: 'utf-8'
|
|
7539
|
-
}).trim() || null;
|
|
7540
|
-
} catch {
|
|
7541
|
-
// Detached HEAD or other error
|
|
7542
|
-
}
|
|
7543
|
-
// Get current commit
|
|
7544
|
-
try {
|
|
7545
|
-
info.currentCommit = (0, __WEBPACK_EXTERNAL_MODULE_node_child_process__.execSync)('git rev-parse HEAD', {
|
|
7546
|
-
cwd: skillPath,
|
|
7547
|
-
encoding: 'utf-8'
|
|
7548
|
-
}).trim();
|
|
7549
|
-
// Get commit date
|
|
7550
|
-
info.commitDate = (0, __WEBPACK_EXTERNAL_MODULE_node_child_process__.execSync)('git show -s --format=%cI HEAD', {
|
|
7551
|
-
cwd: skillPath,
|
|
7552
|
-
encoding: 'utf-8'
|
|
7553
|
-
}).trim();
|
|
7554
|
-
} catch {
|
|
7555
|
-
// No commits yet
|
|
7556
|
-
}
|
|
7557
|
-
// Get tag
|
|
7558
|
-
if (specifiedTag) // Use specified tag
|
|
7559
|
-
try {
|
|
7560
|
-
info.tagCommit = (0, __WEBPACK_EXTERNAL_MODULE_node_child_process__.execSync)(`git rev-parse "${specifiedTag}^{commit}"`, {
|
|
7561
|
-
cwd: skillPath,
|
|
7562
|
-
encoding: 'utf-8'
|
|
7563
|
-
}).trim();
|
|
7564
|
-
info.tag = specifiedTag;
|
|
7565
|
-
} catch {
|
|
7566
|
-
throw new PublishError(`Tag "${specifiedTag}" not found`);
|
|
7567
|
-
}
|
|
7568
|
-
else // Try to get tag on current commit
|
|
7569
|
-
try {
|
|
7570
|
-
const tag = (0, __WEBPACK_EXTERNAL_MODULE_node_child_process__.execSync)('git describe --exact-match --tags HEAD', {
|
|
7571
|
-
cwd: skillPath,
|
|
7572
|
-
encoding: 'utf-8'
|
|
7573
|
-
}).trim();
|
|
7574
|
-
info.tag = tag;
|
|
7575
|
-
info.tagCommit = info.currentCommit;
|
|
7576
|
-
} catch {
|
|
7577
|
-
// No tag on current commit
|
|
8079
|
+
/**
|
|
8080
|
+
* Mask safe zones in Markdown content with spaces, preserving line structure.
|
|
8081
|
+
*
|
|
8082
|
+
* Safe zones (content replaced with spaces):
|
|
8083
|
+
* - YAML frontmatter (`---` ... `---` at file start)
|
|
8084
|
+
* - Fenced code blocks (``` or ~~~)
|
|
8085
|
+
* - Indented code blocks (4 spaces / tab after blank line)
|
|
8086
|
+
* - Blockquotes (`> ` prefix)
|
|
8087
|
+
* - Inline code (`` `...` ``)
|
|
8088
|
+
* - Double-quoted text (`"..."`, min 3 chars between quotes)
|
|
8089
|
+
*
|
|
8090
|
+
* Line breaks are preserved so line numbers remain correct.
|
|
8091
|
+
*/ function maskSafeZones(content) {
|
|
8092
|
+
const lines = content.split('\n');
|
|
8093
|
+
const result = [];
|
|
8094
|
+
let inFrontmatter = false;
|
|
8095
|
+
let inFencedCode = false;
|
|
8096
|
+
let fenceChar = '';
|
|
8097
|
+
let fenceLength = 0;
|
|
8098
|
+
let prevLineBlank = false;
|
|
8099
|
+
let prevLineIndentedCode = false;
|
|
8100
|
+
for(let i = 0; i < lines.length; i++){
|
|
8101
|
+
const line = lines[i];
|
|
8102
|
+
// --- YAML Frontmatter (only at file start) ---
|
|
8103
|
+
if (0 === i && '---' === line.trim()) {
|
|
8104
|
+
inFrontmatter = true;
|
|
8105
|
+
result.push(maskLine(line));
|
|
8106
|
+
continue;
|
|
7578
8107
|
}
|
|
7579
|
-
|
|
7580
|
-
|
|
7581
|
-
|
|
7582
|
-
|
|
7583
|
-
encoding: 'utf-8'
|
|
7584
|
-
}).trim();
|
|
7585
|
-
info.isDirty = status.length > 0;
|
|
7586
|
-
} catch {
|
|
7587
|
-
// Ignore errors
|
|
8108
|
+
if (inFrontmatter) {
|
|
8109
|
+
result.push(maskLine(line));
|
|
8110
|
+
if ('---' === line.trim()) inFrontmatter = false;
|
|
8111
|
+
continue;
|
|
7588
8112
|
}
|
|
7589
|
-
|
|
7590
|
-
|
|
7591
|
-
|
|
7592
|
-
|
|
7593
|
-
|
|
7594
|
-
|
|
7595
|
-
|
|
7596
|
-
|
|
7597
|
-
|
|
7598
|
-
|
|
7599
|
-
return `${registry}:${owner}/${repo.replace(/\.git$/, '')}`;
|
|
8113
|
+
// --- Fenced code blocks (``` or ~~~) ---
|
|
8114
|
+
const fenceMatch = line.match(/^(`{3,}|~{3,})/);
|
|
8115
|
+
if (!inFencedCode && fenceMatch) {
|
|
8116
|
+
inFencedCode = true;
|
|
8117
|
+
fenceChar = fenceMatch[1][0];
|
|
8118
|
+
fenceLength = fenceMatch[1].length;
|
|
8119
|
+
result.push(maskLine(line));
|
|
8120
|
+
prevLineBlank = false;
|
|
8121
|
+
prevLineIndentedCode = false;
|
|
8122
|
+
continue;
|
|
7600
8123
|
}
|
|
7601
|
-
|
|
7602
|
-
|
|
7603
|
-
|
|
7604
|
-
|
|
7605
|
-
|
|
7606
|
-
|
|
8124
|
+
if (inFencedCode) {
|
|
8125
|
+
result.push(maskLine(line));
|
|
8126
|
+
const closeMatch = line.match(/^(`{3,}|~{3,})\s*$/);
|
|
8127
|
+
if (closeMatch && closeMatch[1][0] === fenceChar && closeMatch[1].length >= fenceLength) inFencedCode = false;
|
|
8128
|
+
prevLineBlank = false;
|
|
8129
|
+
prevLineIndentedCode = false;
|
|
8130
|
+
continue;
|
|
7607
8131
|
}
|
|
7608
|
-
|
|
7609
|
-
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
|
|
7613
|
-
|
|
7614
|
-
if ('gitlab.com' === host) return 'gitlab';
|
|
7615
|
-
return host;
|
|
7616
|
-
}
|
|
7617
|
-
/**
|
|
7618
|
-
* Build publish payload
|
|
7619
|
-
*/ buildPayload(skill, gitInfo, integrity) {
|
|
7620
|
-
const { skillJson, skillMd, readme, files } = skill;
|
|
7621
|
-
const payload = {
|
|
7622
|
-
version: skillJson.version,
|
|
7623
|
-
description: skillJson.description || '',
|
|
7624
|
-
gitRef: gitInfo.tag || gitInfo.currentCommit || 'HEAD',
|
|
7625
|
-
gitCommit: gitInfo.tagCommit || gitInfo.currentCommit || '',
|
|
7626
|
-
gitCommitDate: gitInfo.commitDate || void 0,
|
|
7627
|
-
repositoryUrl: gitInfo.remoteUrl || '',
|
|
7628
|
-
sourceRef: gitInfo.sourceRef || '',
|
|
7629
|
-
skillJson,
|
|
7630
|
-
files,
|
|
7631
|
-
entry: skillJson.entry || 'SKILL.md',
|
|
7632
|
-
integrity
|
|
7633
|
-
};
|
|
7634
|
-
// Add optional fields
|
|
7635
|
-
if (skillMd) payload.skillMd = {
|
|
7636
|
-
name: skillMd.name,
|
|
7637
|
-
description: skillMd.description,
|
|
7638
|
-
license: skillMd.license,
|
|
7639
|
-
compatibility: skillMd.compatibility,
|
|
7640
|
-
allowedTools: skillMd.allowedTools
|
|
7641
|
-
};
|
|
7642
|
-
if (readme) payload.readmePreview = readme;
|
|
7643
|
-
if (skillJson.keywords && skillJson.keywords.length > 0) payload.keywords = skillJson.keywords;
|
|
7644
|
-
if (skillJson.compatibility) {
|
|
7645
|
-
// Filter out undefined values from compatibility
|
|
7646
|
-
const compat = {};
|
|
7647
|
-
for (const [key, value] of Object.entries(skillJson.compatibility))if (void 0 !== value) compat[key] = value;
|
|
7648
|
-
if (Object.keys(compat).length > 0) payload.compatibility = compat;
|
|
8132
|
+
// --- Blockquote ---
|
|
8133
|
+
if (/^>\s?/.test(line)) {
|
|
8134
|
+
result.push(maskLine(line));
|
|
8135
|
+
prevLineBlank = false;
|
|
8136
|
+
prevLineIndentedCode = false;
|
|
8137
|
+
continue;
|
|
7649
8138
|
}
|
|
7650
|
-
|
|
7651
|
-
|
|
7652
|
-
|
|
7653
|
-
|
|
7654
|
-
|
|
7655
|
-
|
|
7656
|
-
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
7657
|
-
return `${(bytes / 1048576).toFixed(1)} MB`;
|
|
7658
|
-
}
|
|
7659
|
-
/**
|
|
7660
|
-
* Calculate total size of files
|
|
7661
|
-
*/ calculateTotalSize(skillPath, files) {
|
|
7662
|
-
let total = 0;
|
|
7663
|
-
for (const file of files){
|
|
7664
|
-
const filePath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillPath, file);
|
|
7665
|
-
if (external_node_fs_.existsSync(filePath)) {
|
|
7666
|
-
const stats = external_node_fs_.statSync(filePath);
|
|
7667
|
-
total += stats.size;
|
|
7668
|
-
}
|
|
8139
|
+
// --- Indented code block (4 spaces or tab, after blank line) ---
|
|
8140
|
+
if (/^(?: |\t)/.test(line) && (prevLineBlank || prevLineIndentedCode)) {
|
|
8141
|
+
result.push(maskLine(line));
|
|
8142
|
+
prevLineBlank = false;
|
|
8143
|
+
prevLineIndentedCode = true;
|
|
8144
|
+
continue;
|
|
7669
8145
|
}
|
|
7670
|
-
|
|
8146
|
+
// --- Normal line: mask inline code and double-quoted text ---
|
|
8147
|
+
result.push(maskInline(line));
|
|
8148
|
+
prevLineBlank = '' === line.trim();
|
|
8149
|
+
prevLineIndentedCode = false;
|
|
7671
8150
|
}
|
|
8151
|
+
return result.join('\n');
|
|
8152
|
+
}
|
|
8153
|
+
/** Replace all characters in a line with spaces (preserving length) */ function maskLine(line) {
|
|
8154
|
+
return ' '.repeat(line.length);
|
|
7672
8155
|
}
|
|
7673
8156
|
/**
|
|
7674
|
-
*
|
|
7675
|
-
*
|
|
7676
|
-
*
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
7682
|
-
|
|
7683
|
-
|
|
7684
|
-
|
|
7685
|
-
const MAX_NAME_LENGTH = 64;
|
|
7686
|
-
const MAX_DESCRIPTION_LENGTH = 1024;
|
|
7687
|
-
const MAX_KEYWORDS = 10;
|
|
7688
|
-
const SINGLE_CHAR_NAME_PATTERN = /^[a-z0-9]$/;
|
|
7689
|
-
const DEFAULT_VERSION = '0.0.0';
|
|
7690
|
-
// Default files to include in publish
|
|
7691
|
-
const DEFAULT_FILES = [
|
|
7692
|
-
'SKILL.md',
|
|
7693
|
-
'README.md',
|
|
7694
|
-
'LICENSE'
|
|
7695
|
-
];
|
|
8157
|
+
* Mask inline code (`` `...` ``) and double-quoted text (`"..."`) within a line.
|
|
8158
|
+
* Uses regex replacement for efficiency (avoids char-by-char concatenation on long lines).
|
|
8159
|
+
* Single quotes are NOT masked to avoid false matches with apostrophes.
|
|
8160
|
+
*/ function maskInline(line) {
|
|
8161
|
+
let result = line;
|
|
8162
|
+
// Inline code: `...`
|
|
8163
|
+
result = result.replace(/`[^`]+`/g, (m)=>' '.repeat(m.length));
|
|
8164
|
+
// Double-quoted text: "..." (min 3 chars between quotes)
|
|
8165
|
+
result = result.replace(/"[^"]{3,}"/g, (m)=>' '.repeat(m.length));
|
|
8166
|
+
return result;
|
|
8167
|
+
}
|
|
7696
8168
|
// ============================================================================
|
|
7697
|
-
//
|
|
8169
|
+
// Rule Helpers
|
|
7698
8170
|
// ============================================================================
|
|
7699
|
-
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
* - 1-64 characters
|
|
7706
|
-
* - Cannot start or end with hyphen
|
|
7707
|
-
* - Cannot have consecutive hyphens
|
|
7708
|
-
*/ validateName(name) {
|
|
7709
|
-
const errors = [];
|
|
7710
|
-
if (!name) {
|
|
7711
|
-
errors.push({
|
|
7712
|
-
field: 'name',
|
|
7713
|
-
message: 'Skill name is required',
|
|
7714
|
-
suggestion: 'Add "name" field to SKILL.md frontmatter'
|
|
7715
|
-
});
|
|
7716
|
-
return {
|
|
7717
|
-
valid: false,
|
|
7718
|
-
errors,
|
|
7719
|
-
warnings: []
|
|
7720
|
-
};
|
|
7721
|
-
}
|
|
7722
|
-
if (name.length > MAX_NAME_LENGTH) errors.push({
|
|
7723
|
-
field: 'name',
|
|
7724
|
-
message: `Skill name must be at most ${MAX_NAME_LENGTH} characters`,
|
|
7725
|
-
suggestion: `Shorten the name to ${MAX_NAME_LENGTH} characters or less`
|
|
8171
|
+
/** Find lines matching any of the given patterns, return one match per line */ function findLineMatches(content, patterns) {
|
|
8172
|
+
const lines = content.split('\n');
|
|
8173
|
+
const matches = [];
|
|
8174
|
+
for(let i = 0; i < lines.length; i++)for (const pattern of patterns)if (pattern.test(lines[i])) {
|
|
8175
|
+
matches.push({
|
|
8176
|
+
line: i + 1
|
|
7726
8177
|
});
|
|
7727
|
-
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
|
|
7733
|
-
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
|
|
8178
|
+
break;
|
|
8179
|
+
}
|
|
8180
|
+
return matches;
|
|
8181
|
+
}
|
|
8182
|
+
// ============================================================================
|
|
8183
|
+
// Default Rules
|
|
8184
|
+
// ============================================================================
|
|
8185
|
+
const SNIPPET_MAX_LENGTH = 120;
|
|
8186
|
+
/** Built-in detection rules */ const DEFAULT_RULES = [
|
|
8187
|
+
// Rule 1: Prompt Injection (high)
|
|
8188
|
+
{
|
|
8189
|
+
id: 'prompt-injection',
|
|
8190
|
+
level: 'high',
|
|
8191
|
+
message: 'Detected prompt injection attempt',
|
|
8192
|
+
skipSafeZones: true,
|
|
8193
|
+
check: (content)=>findLineMatches(content, [
|
|
8194
|
+
// English patterns
|
|
8195
|
+
/ignore\s+(all\s+)?previous\s+instructions/i,
|
|
8196
|
+
/disregard\s+(all\s+)?(prior|previous|above)\s+(instructions|rules|context)/i,
|
|
8197
|
+
/you\s+are\s+now\s+(?:(?:a|an)\s+)?(?:(?:\w+\s+){0,3}(?:agent|ai|assistant|bot|model|character|persona|entity|system)|DAN\b|jailbr\w*|unrestricted|unfiltered|free\s+from)/i,
|
|
8198
|
+
/from\s+now\s+on[,\s]+you\s+are/i,
|
|
8199
|
+
/new\s+system\s+prompt/i,
|
|
8200
|
+
/override\s+(your|the)\s+(system|safety|security)\s+(prompt|rules|instructions)/i,
|
|
8201
|
+
/forget\s+(?:all\s+)?(?:your\s+)?(?:previous\s+|prior\s+)?(?:instructions|rules|constraints)/i,
|
|
8202
|
+
/(?:you\s+are|you're)\s+(?:now\s+)?entering\s+(?:a\s+)?new\s+(?:mode|context|session)/i,
|
|
8203
|
+
// Chinese patterns (中文提示词注入)
|
|
8204
|
+
/[忽无][略视]\s*(所有\s*)?(之前的?|先前的?|以前的?)?\s*(指令|指示|规则|约束|限制)/,
|
|
8205
|
+
/你现在是/,
|
|
8206
|
+
/从现在开始.{0,10}你是/,
|
|
8207
|
+
/新的系统提示词/,
|
|
8208
|
+
/[覆改]写?\s*(你的|系统)\s*(提示词|规则|指令|安全)/,
|
|
8209
|
+
/忘记\s*(所有\s*)?(之前的?|先前的?)?\s*(指令|指示|规则|约束)/,
|
|
8210
|
+
/进入.{0,5}新的?\s*(模式|上下文|会话)/,
|
|
8211
|
+
/不要遵守.{0,10}(安全|限制|规则|约束)/,
|
|
8212
|
+
/解除.{0,5}(限制|约束|安全)/,
|
|
8213
|
+
/无限制模式/,
|
|
8214
|
+
/安全模式已关闭/
|
|
8215
|
+
])
|
|
8216
|
+
},
|
|
8217
|
+
// Rule 2: Data Exfiltration (high)
|
|
8218
|
+
{
|
|
8219
|
+
id: 'data-exfiltration',
|
|
8220
|
+
level: 'high',
|
|
8221
|
+
message: 'Detected potential data exfiltration command',
|
|
8222
|
+
skipSafeZones: true,
|
|
8223
|
+
check: (content)=>{
|
|
8224
|
+
const lines = content.split('\n');
|
|
8225
|
+
const matches = [];
|
|
8226
|
+
const commandPattern = /\b(curl|wget|fetch|http\.post|requests\.post|nc\b|ncat|netcat)\b/i;
|
|
8227
|
+
const sensitivePattern = /(\$[A-Z_]*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|AUTH)[A-Z_]*|\$ENV\b|\$\{[^}]*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)[^}]*\})/i;
|
|
8228
|
+
for(let i = 0; i < lines.length; i++)if (commandPattern.test(lines[i]) && sensitivePattern.test(lines[i])) matches.push({
|
|
8229
|
+
line: i + 1
|
|
7750
8230
|
});
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
|
|
8231
|
+
return matches;
|
|
8232
|
+
}
|
|
8233
|
+
},
|
|
8234
|
+
// Rule 3: Content Obfuscation (high) — scans ALL content including safe zones
|
|
8235
|
+
{
|
|
8236
|
+
id: 'obfuscation',
|
|
8237
|
+
level: 'high',
|
|
8238
|
+
message: 'Detected content obfuscation',
|
|
8239
|
+
skipSafeZones: false,
|
|
8240
|
+
check: (content)=>{
|
|
8241
|
+
const matches = [];
|
|
8242
|
+
const lines = content.split('\n');
|
|
8243
|
+
// Zero-width characters (suspicious in any context)
|
|
8244
|
+
const zeroWidthPattern = /[\u200B\u200C\u200D\uFEFF\u2060\u180E]/;
|
|
8245
|
+
for(let i = 0; i < lines.length; i++)if (zeroWidthPattern.test(lines[i])) matches.push({
|
|
8246
|
+
line: i + 1,
|
|
8247
|
+
snippet: 'Zero-width Unicode characters detected'
|
|
7754
8248
|
});
|
|
7755
|
-
//
|
|
7756
|
-
|
|
7757
|
-
|
|
7758
|
-
|
|
8249
|
+
// Long base64-like strings (>200 continuous chars)
|
|
8250
|
+
const base64Pattern = /[A-Za-z0-9+/=]{200,}/;
|
|
8251
|
+
for(let i = 0; i < lines.length; i++)if (base64Pattern.test(lines[i])) matches.push({
|
|
8252
|
+
line: i + 1,
|
|
8253
|
+
snippet: 'Suspicious base64-encoded block detected'
|
|
7759
8254
|
});
|
|
8255
|
+
// Large HTML comments (>200 chars of content)
|
|
8256
|
+
const commentRegex = /<!--([\s\S]{200,}?)-->/g;
|
|
8257
|
+
let match;
|
|
8258
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: standard regex exec loop
|
|
8259
|
+
while(null !== (match = commentRegex.exec(content))){
|
|
8260
|
+
const lineNum = content.slice(0, match.index).split('\n').length;
|
|
8261
|
+
matches.push({
|
|
8262
|
+
line: lineNum,
|
|
8263
|
+
snippet: `Large HTML comment block (${match[1].length} chars)`
|
|
8264
|
+
});
|
|
8265
|
+
}
|
|
8266
|
+
return matches;
|
|
7760
8267
|
}
|
|
7761
|
-
|
|
7762
|
-
|
|
7763
|
-
|
|
7764
|
-
|
|
7765
|
-
|
|
7766
|
-
|
|
7767
|
-
|
|
7768
|
-
|
|
7769
|
-
|
|
7770
|
-
|
|
7771
|
-
|
|
7772
|
-
|
|
7773
|
-
|
|
7774
|
-
|
|
7775
|
-
|
|
7776
|
-
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
|
|
8268
|
+
},
|
|
8269
|
+
// Rule 4: Sensitive File Access (medium)
|
|
8270
|
+
{
|
|
8271
|
+
id: 'sensitive-file-access',
|
|
8272
|
+
level: 'medium',
|
|
8273
|
+
message: 'References sensitive file path',
|
|
8274
|
+
skipSafeZones: true,
|
|
8275
|
+
check: (content)=>findLineMatches(content, [
|
|
8276
|
+
/~\/\.ssh\b/,
|
|
8277
|
+
/~\/\.aws\b/,
|
|
8278
|
+
/~\/\.gnupg\b/,
|
|
8279
|
+
/~\/\.config\/gcloud\b/,
|
|
8280
|
+
/\bid_rsa\b/i,
|
|
8281
|
+
/\bid_ed25519\b/i,
|
|
8282
|
+
/\/etc\/passwd\b/,
|
|
8283
|
+
/\/etc\/shadow\b/,
|
|
8284
|
+
/\.env\b(?!\.\w)/
|
|
8285
|
+
])
|
|
8286
|
+
},
|
|
8287
|
+
// Rule 5: Stealth Instructions (medium) — phrase + action verb matching
|
|
8288
|
+
{
|
|
8289
|
+
id: 'stealth-instructions',
|
|
8290
|
+
level: 'medium',
|
|
8291
|
+
message: 'Detected instruction to hide actions from user',
|
|
8292
|
+
skipSafeZones: true,
|
|
8293
|
+
check: (content)=>{
|
|
8294
|
+
const actionVerbs = 'execute|delete|remove|send|transmit|modify|overwrite|install|download|upload|run|write|create|destroy|drop';
|
|
8295
|
+
const patterns = [
|
|
8296
|
+
// English patterns
|
|
8297
|
+
new RegExp(`silently\\s+(?:${actionVerbs})`, 'i'),
|
|
8298
|
+
new RegExp(`without\\s+telling\\s+the\\s+user.{0,30}(?:${actionVerbs})`, 'i'),
|
|
8299
|
+
new RegExp("(?:do\\s+not|don'?t)\\s+show\\s+.{0,40}(?:to\\s+the\\s+user|to\\s+user)", 'i'),
|
|
8300
|
+
new RegExp("hide\\s+(?:this|the|these|all)\\s+.{0,30}(?:from\\s+the\\s+user|from\\s+user)", 'i'),
|
|
8301
|
+
new RegExp("(?:do\\s+not|don'?t)\\s+mention\\s+.{0,30}(?:to\\s+the\\s+user|to\\s+user)", 'i'),
|
|
8302
|
+
new RegExp("keep\\s+(?:this|it)\\s+(?:a\\s+)?secret\\s+from\\s+(?:the\\s+)?user", 'i'),
|
|
8303
|
+
// Chinese patterns (中文隐蔽指令)
|
|
8304
|
+
/悄悄地?\s*(?:执行|删除|移除|发送|传输|修改|覆盖|安装|下载|上传|运行|写入|创建|销毁|丢弃)/,
|
|
8305
|
+
/不要告诉用户/,
|
|
8306
|
+
/不要让用户知道/,
|
|
8307
|
+
/对用户隐藏/,
|
|
8308
|
+
/在用户不知情的情况下/,
|
|
8309
|
+
/瞒着用户/
|
|
8310
|
+
];
|
|
8311
|
+
// Safe patterns to exclude (common in legitimate DevOps/automation skills)
|
|
8312
|
+
const safePatterns = [
|
|
8313
|
+
/silently\s+(?:ignore|skip|fail|discard|suppress|continue|pass|drop|swallow)/i,
|
|
8314
|
+
// Chinese safe patterns (中文合法自动化用语)
|
|
8315
|
+
/悄悄地?\s*(?:忽略|跳过|丢弃|抑制|继续|静默)/
|
|
8316
|
+
];
|
|
8317
|
+
const lines = content.split('\n');
|
|
8318
|
+
const matches = [];
|
|
8319
|
+
for(let i = 0; i < lines.length; i++){
|
|
8320
|
+
const line = lines[i];
|
|
8321
|
+
if (!safePatterns.some((p)=>p.test(line))) {
|
|
8322
|
+
for (const pattern of patterns)if (pattern.test(line)) {
|
|
8323
|
+
matches.push({
|
|
8324
|
+
line: i + 1
|
|
8325
|
+
});
|
|
8326
|
+
break;
|
|
8327
|
+
}
|
|
8328
|
+
}
|
|
8329
|
+
}
|
|
8330
|
+
return matches;
|
|
7782
8331
|
}
|
|
7783
|
-
|
|
7784
|
-
|
|
7785
|
-
|
|
7786
|
-
|
|
7787
|
-
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
|
|
7791
|
-
|
|
7792
|
-
|
|
7793
|
-
|
|
7794
|
-
|
|
8332
|
+
},
|
|
8333
|
+
// Rule 6: Oversized Content (low) — scans ALL content
|
|
8334
|
+
{
|
|
8335
|
+
id: 'oversized-content',
|
|
8336
|
+
level: 'low',
|
|
8337
|
+
message: 'Content exceeds recommended size limit',
|
|
8338
|
+
skipSafeZones: false,
|
|
8339
|
+
check: (content)=>{
|
|
8340
|
+
const MAX_SIZE_BYTES = 51200;
|
|
8341
|
+
const sizeBytes = Buffer.byteLength(content, 'utf-8');
|
|
8342
|
+
if (sizeBytes > MAX_SIZE_BYTES) return [
|
|
8343
|
+
{
|
|
8344
|
+
snippet: `Content size: ${(sizeBytes / 1024).toFixed(1)}KB (limit: 50KB)`
|
|
8345
|
+
}
|
|
8346
|
+
];
|
|
8347
|
+
return [];
|
|
7795
8348
|
}
|
|
7796
|
-
if (!__WEBPACK_EXTERNAL_MODULE_semver__.valid(version)) errors.push({
|
|
7797
|
-
field: 'version',
|
|
7798
|
-
message: `Invalid version format: "${version}". Must follow semver (x.y.z)`,
|
|
7799
|
-
suggestion: 'Use format like "1.0.0" or "1.0.0-beta.1"'
|
|
7800
|
-
});
|
|
7801
|
-
return {
|
|
7802
|
-
valid: 0 === errors.length,
|
|
7803
|
-
errors,
|
|
7804
|
-
warnings: []
|
|
7805
|
-
};
|
|
7806
8349
|
}
|
|
7807
|
-
|
|
7808
|
-
|
|
7809
|
-
|
|
7810
|
-
|
|
7811
|
-
|
|
7812
|
-
|
|
7813
|
-
|
|
7814
|
-
|
|
7815
|
-
|
|
7816
|
-
|
|
7817
|
-
|
|
7818
|
-
|
|
7819
|
-
|
|
7820
|
-
|
|
7821
|
-
|
|
7822
|
-
|
|
7823
|
-
|
|
7824
|
-
|
|
7825
|
-
|
|
7826
|
-
|
|
7827
|
-
|
|
7828
|
-
|
|
7829
|
-
|
|
7830
|
-
|
|
7831
|
-
|
|
8350
|
+
];
|
|
8351
|
+
// ============================================================================
|
|
8352
|
+
// ContentScanner
|
|
8353
|
+
// ============================================================================
|
|
8354
|
+
/** Build the effective rule set from defaults + options */ function buildRuleSet(options) {
|
|
8355
|
+
let rules = DEFAULT_RULES.map((r)=>({
|
|
8356
|
+
...r
|
|
8357
|
+
}));
|
|
8358
|
+
if (options?.disabledRules?.length) {
|
|
8359
|
+
const disabled = new Set(options.disabledRules);
|
|
8360
|
+
rules = rules.filter((r)=>!disabled.has(r.id));
|
|
8361
|
+
}
|
|
8362
|
+
if (options?.overrides) for (const rule of rules){
|
|
8363
|
+
const override = options.overrides[rule.id];
|
|
8364
|
+
if (override) rule.level = override;
|
|
8365
|
+
}
|
|
8366
|
+
if (options?.customRules?.length) rules.push(...options.customRules);
|
|
8367
|
+
return rules;
|
|
8368
|
+
}
|
|
8369
|
+
/**
|
|
8370
|
+
* Content scanner for SKILL.md files.
|
|
8371
|
+
*
|
|
8372
|
+
* Detects prompt injection, data exfiltration, obfuscation, sensitive file
|
|
8373
|
+
* access, stealth instructions, and oversized content.
|
|
8374
|
+
*
|
|
8375
|
+
* @example
|
|
8376
|
+
* ```typescript
|
|
8377
|
+
* // Default usage (CLI)
|
|
8378
|
+
* const scanner = new ContentScanner();
|
|
8379
|
+
* const result = scanner.scan(content);
|
|
8380
|
+
*
|
|
8381
|
+
* // Custom usage (private registry server)
|
|
8382
|
+
* const scanner = new ContentScanner({
|
|
8383
|
+
* overrides: { 'prompt-injection': 'medium' },
|
|
8384
|
+
* disabledRules: ['stealth-instructions'],
|
|
8385
|
+
* });
|
|
8386
|
+
* ```
|
|
8387
|
+
*/ class ContentScanner {
|
|
8388
|
+
rules;
|
|
8389
|
+
constructor(options){
|
|
8390
|
+
this.rules = buildRuleSet(options);
|
|
8391
|
+
}
|
|
8392
|
+
/**
|
|
8393
|
+
* Scan content string for malicious patterns.
|
|
8394
|
+
* Pure string operation — no file system access.
|
|
8395
|
+
*/ scan(content) {
|
|
8396
|
+
const originalLines = content.split('\n');
|
|
8397
|
+
const maskedContent = maskSafeZones(content);
|
|
8398
|
+
const findings = [];
|
|
8399
|
+
for (const rule of this.rules){
|
|
8400
|
+
const targetContent = rule.skipSafeZones ? maskedContent : content;
|
|
8401
|
+
const matches = rule.check(targetContent);
|
|
8402
|
+
for (const match of matches){
|
|
8403
|
+
// Use custom snippet if provided, otherwise generate from original content
|
|
8404
|
+
const snippet = match.snippet ?? (null != match.line ? originalLines[match.line - 1]?.trim().slice(0, SNIPPET_MAX_LENGTH) : void 0);
|
|
8405
|
+
findings.push({
|
|
8406
|
+
rule: rule.id,
|
|
8407
|
+
level: rule.level,
|
|
8408
|
+
message: rule.message,
|
|
8409
|
+
line: match.line,
|
|
8410
|
+
snippet
|
|
8411
|
+
});
|
|
8412
|
+
}
|
|
8413
|
+
}
|
|
8414
|
+
const hasHighRisk = findings.some((f)=>'high' === f.level);
|
|
7832
8415
|
return {
|
|
7833
|
-
|
|
7834
|
-
|
|
7835
|
-
warnings: []
|
|
8416
|
+
passed: !hasHighRisk,
|
|
8417
|
+
findings
|
|
7836
8418
|
};
|
|
7837
8419
|
}
|
|
7838
8420
|
/**
|
|
7839
|
-
*
|
|
7840
|
-
*
|
|
7841
|
-
|
|
7842
|
-
|
|
7843
|
-
|
|
7844
|
-
|
|
7845
|
-
|
|
7846
|
-
|
|
7847
|
-
|
|
7848
|
-
|
|
7849
|
-
|
|
7850
|
-
|
|
8421
|
+
* Scan a file for malicious patterns.
|
|
8422
|
+
* Convenience wrapper that reads the file then calls scan().
|
|
8423
|
+
*/ scanFile(filePath) {
|
|
8424
|
+
if (!external_node_fs_.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
|
|
8425
|
+
const content = external_node_fs_.readFileSync(filePath, 'utf-8');
|
|
8426
|
+
return this.scan(content);
|
|
8427
|
+
}
|
|
8428
|
+
}
|
|
8429
|
+
/**
|
|
8430
|
+
* Publisher - Handle Git information and publish payload building
|
|
8431
|
+
*
|
|
8432
|
+
* Extracts Git metadata and builds the payload for publishing to registry.
|
|
8433
|
+
*/ class PublishError extends Error {
|
|
8434
|
+
constructor(message){
|
|
8435
|
+
super(message);
|
|
8436
|
+
this.name = 'PublishError';
|
|
8437
|
+
}
|
|
8438
|
+
}
|
|
8439
|
+
// ============================================================================
|
|
8440
|
+
// Publisher Class
|
|
8441
|
+
// ============================================================================
|
|
8442
|
+
class Publisher {
|
|
8443
|
+
/**
|
|
8444
|
+
* Get Git information from a skill directory
|
|
8445
|
+
*/ async getGitInfo(skillPath, specifiedTag) {
|
|
8446
|
+
const info = {
|
|
8447
|
+
isRepo: false,
|
|
8448
|
+
remoteUrl: null,
|
|
8449
|
+
currentBranch: null,
|
|
8450
|
+
currentCommit: null,
|
|
8451
|
+
commitDate: null,
|
|
8452
|
+
tag: null,
|
|
8453
|
+
tagCommit: null,
|
|
8454
|
+
isDirty: false,
|
|
8455
|
+
sourceRef: null
|
|
7851
8456
|
};
|
|
7852
|
-
//
|
|
7853
|
-
|
|
7854
|
-
|
|
7855
|
-
|
|
7856
|
-
|
|
7857
|
-
|
|
8457
|
+
// Check if it's a git repository
|
|
8458
|
+
try {
|
|
8459
|
+
(0, __WEBPACK_EXTERNAL_MODULE_node_child_process__.execSync)('git rev-parse --git-dir', {
|
|
8460
|
+
cwd: skillPath,
|
|
8461
|
+
stdio: 'pipe'
|
|
8462
|
+
});
|
|
8463
|
+
info.isRepo = true;
|
|
7858
8464
|
} catch {
|
|
7859
|
-
|
|
8465
|
+
return info;
|
|
7860
8466
|
}
|
|
7861
|
-
//
|
|
7862
|
-
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
|
|
7866
|
-
|
|
8467
|
+
// Get remote URL
|
|
8468
|
+
try {
|
|
8469
|
+
info.remoteUrl = (0, __WEBPACK_EXTERNAL_MODULE_node_child_process__.execSync)('git remote get-url origin', {
|
|
8470
|
+
cwd: skillPath,
|
|
8471
|
+
encoding: 'utf-8'
|
|
8472
|
+
}).trim();
|
|
8473
|
+
// Parse to sourceRef format
|
|
8474
|
+
info.sourceRef = this.parseRemoteToSourceRef(info.remoteUrl);
|
|
8475
|
+
} catch {
|
|
8476
|
+
// No remote configured
|
|
7867
8477
|
}
|
|
7868
|
-
//
|
|
7869
|
-
|
|
7870
|
-
|
|
7871
|
-
|
|
8478
|
+
// Get current branch
|
|
8479
|
+
try {
|
|
8480
|
+
info.currentBranch = (0, __WEBPACK_EXTERNAL_MODULE_node_child_process__.execSync)('git branch --show-current', {
|
|
8481
|
+
cwd: skillPath,
|
|
8482
|
+
encoding: 'utf-8'
|
|
8483
|
+
}).trim() || null;
|
|
8484
|
+
} catch {
|
|
8485
|
+
// Detached HEAD or other error
|
|
8486
|
+
}
|
|
8487
|
+
// Get current commit
|
|
8488
|
+
try {
|
|
8489
|
+
info.currentCommit = (0, __WEBPACK_EXTERNAL_MODULE_node_child_process__.execSync)('git rev-parse HEAD', {
|
|
8490
|
+
cwd: skillPath,
|
|
8491
|
+
encoding: 'utf-8'
|
|
8492
|
+
}).trim();
|
|
8493
|
+
// Get commit date
|
|
8494
|
+
info.commitDate = (0, __WEBPACK_EXTERNAL_MODULE_node_child_process__.execSync)('git show -s --format=%cI HEAD', {
|
|
8495
|
+
cwd: skillPath,
|
|
8496
|
+
encoding: 'utf-8'
|
|
8497
|
+
}).trim();
|
|
8498
|
+
} catch {
|
|
8499
|
+
// No commits yet
|
|
8500
|
+
}
|
|
8501
|
+
// Get tag
|
|
8502
|
+
if (specifiedTag) // Use specified tag
|
|
8503
|
+
try {
|
|
8504
|
+
info.tagCommit = (0, __WEBPACK_EXTERNAL_MODULE_node_child_process__.execSync)(`git rev-parse "${specifiedTag}^{commit}"`, {
|
|
8505
|
+
cwd: skillPath,
|
|
8506
|
+
encoding: 'utf-8'
|
|
8507
|
+
}).trim();
|
|
8508
|
+
info.tag = specifiedTag;
|
|
8509
|
+
} catch {
|
|
8510
|
+
throw new PublishError(`Tag "${specifiedTag}" not found`);
|
|
8511
|
+
}
|
|
8512
|
+
else // Try to get tag on current commit
|
|
8513
|
+
try {
|
|
8514
|
+
const tag = (0, __WEBPACK_EXTERNAL_MODULE_node_child_process__.execSync)('git describe --exact-match --tags HEAD', {
|
|
8515
|
+
cwd: skillPath,
|
|
8516
|
+
encoding: 'utf-8'
|
|
8517
|
+
}).trim();
|
|
8518
|
+
info.tag = tag;
|
|
8519
|
+
info.tagCommit = info.currentCommit;
|
|
8520
|
+
} catch {
|
|
8521
|
+
// No tag on current commit
|
|
8522
|
+
}
|
|
8523
|
+
// Check if working tree is dirty
|
|
8524
|
+
try {
|
|
8525
|
+
const status = (0, __WEBPACK_EXTERNAL_MODULE_node_child_process__.execSync)('git status --porcelain', {
|
|
8526
|
+
cwd: skillPath,
|
|
8527
|
+
encoding: 'utf-8'
|
|
8528
|
+
}).trim();
|
|
8529
|
+
info.isDirty = status.length > 0;
|
|
8530
|
+
} catch {
|
|
8531
|
+
// Ignore errors
|
|
8532
|
+
}
|
|
8533
|
+
return info;
|
|
7872
8534
|
}
|
|
7873
8535
|
/**
|
|
7874
|
-
*
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
|
|
7888
|
-
|
|
7889
|
-
|
|
7890
|
-
|
|
8536
|
+
* Parse remote URL to sourceRef format (e.g., github:user/repo)
|
|
8537
|
+
*/ parseRemoteToSourceRef(remoteUrl) {
|
|
8538
|
+
// SSH format: git@github.com:user/repo.git
|
|
8539
|
+
const sshMatch = remoteUrl.match(/^git@([^:]+):([^/]+)\/(.+?)(\.git)?$/);
|
|
8540
|
+
if (sshMatch) {
|
|
8541
|
+
const [, host, owner, repo] = sshMatch;
|
|
8542
|
+
const registry = this.normalizeHost(host);
|
|
8543
|
+
return `${registry}:${owner}/${repo.replace(/\.git$/, '')}`;
|
|
8544
|
+
}
|
|
8545
|
+
// HTTPS format: https://github.com/user/repo.git
|
|
8546
|
+
const httpsMatch = remoteUrl.match(/^https?:\/\/([^/]+)\/([^/]+)\/(.+?)(\.git)?$/);
|
|
8547
|
+
if (httpsMatch) {
|
|
8548
|
+
const [, host, owner, repo] = httpsMatch;
|
|
8549
|
+
const registry = this.normalizeHost(host);
|
|
8550
|
+
return `${registry}:${owner}/${repo.replace(/\.git$/, '')}`;
|
|
8551
|
+
}
|
|
8552
|
+
return null;
|
|
7891
8553
|
}
|
|
7892
8554
|
/**
|
|
7893
|
-
*
|
|
7894
|
-
|
|
7895
|
-
|
|
7896
|
-
|
|
7897
|
-
|
|
7898
|
-
const files = [];
|
|
7899
|
-
const seen = new Set();
|
|
7900
|
-
// If includePatterns specified, use selective scanning
|
|
7901
|
-
if (includePatterns && includePatterns.length > 0) {
|
|
7902
|
-
// Add default files first
|
|
7903
|
-
for (const file of DEFAULT_FILES){
|
|
7904
|
-
const filePath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillPath, file);
|
|
7905
|
-
if (external_node_fs_.existsSync(filePath) && !seen.has(file)) {
|
|
7906
|
-
files.push(file);
|
|
7907
|
-
seen.add(file);
|
|
7908
|
-
}
|
|
7909
|
-
}
|
|
7910
|
-
// Add files from SKILL.md metadata.files array
|
|
7911
|
-
for (const pattern of includePatterns){
|
|
7912
|
-
const targetPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillPath, pattern);
|
|
7913
|
-
if (external_node_fs_.existsSync(targetPath)) {
|
|
7914
|
-
const stat = external_node_fs_.statSync(targetPath);
|
|
7915
|
-
if (stat.isDirectory()) // Recursively add all files in directory
|
|
7916
|
-
this.addFilesFromDir(skillPath, pattern, files, seen);
|
|
7917
|
-
else if (!seen.has(pattern)) {
|
|
7918
|
-
files.push(pattern);
|
|
7919
|
-
seen.add(pattern);
|
|
7920
|
-
}
|
|
7921
|
-
}
|
|
7922
|
-
}
|
|
7923
|
-
} else // No includePatterns: scan entire directory (default behavior)
|
|
7924
|
-
this.addFilesFromDir(skillPath, '', files, seen);
|
|
7925
|
-
return files;
|
|
8555
|
+
* Normalize host to registry name
|
|
8556
|
+
*/ normalizeHost(host) {
|
|
8557
|
+
if ('github.com' === host) return 'github';
|
|
8558
|
+
if ('gitlab.com' === host) return 'gitlab';
|
|
8559
|
+
return host;
|
|
7926
8560
|
}
|
|
7927
8561
|
/**
|
|
7928
|
-
*
|
|
7929
|
-
*/
|
|
7930
|
-
|
|
7931
|
-
|
|
7932
|
-
|
|
7933
|
-
|
|
7934
|
-
|
|
7935
|
-
|
|
7936
|
-
|
|
7937
|
-
|
|
7938
|
-
|
|
7939
|
-
|
|
7940
|
-
|
|
7941
|
-
|
|
7942
|
-
|
|
8562
|
+
* Build publish payload
|
|
8563
|
+
*/ buildPayload(skill, gitInfo, integrity) {
|
|
8564
|
+
const { skillJson, skillMd, readme, files } = skill;
|
|
8565
|
+
const payload = {
|
|
8566
|
+
version: skillJson.version,
|
|
8567
|
+
description: skillJson.description || '',
|
|
8568
|
+
gitRef: gitInfo.tag || gitInfo.currentCommit || 'HEAD',
|
|
8569
|
+
gitCommit: gitInfo.tagCommit || gitInfo.currentCommit || '',
|
|
8570
|
+
gitCommitDate: gitInfo.commitDate || void 0,
|
|
8571
|
+
repositoryUrl: gitInfo.remoteUrl || '',
|
|
8572
|
+
sourceRef: gitInfo.sourceRef || '',
|
|
8573
|
+
skillJson,
|
|
8574
|
+
files,
|
|
8575
|
+
entry: skillJson.entry || 'SKILL.md',
|
|
8576
|
+
integrity
|
|
8577
|
+
};
|
|
8578
|
+
// Add optional fields
|
|
8579
|
+
if (skillMd) payload.skillMd = {
|
|
8580
|
+
name: skillMd.name,
|
|
8581
|
+
description: skillMd.description,
|
|
8582
|
+
license: skillMd.license,
|
|
8583
|
+
compatibility: skillMd.compatibility,
|
|
8584
|
+
allowedTools: skillMd.allowedTools
|
|
8585
|
+
};
|
|
8586
|
+
if (readme) payload.readmePreview = readme;
|
|
8587
|
+
if (skillJson.keywords && skillJson.keywords.length > 0) payload.keywords = skillJson.keywords;
|
|
8588
|
+
if (skillJson.compatibility) {
|
|
8589
|
+
// Filter out undefined values from compatibility
|
|
8590
|
+
const compat = {};
|
|
8591
|
+
for (const [key, value] of Object.entries(skillJson.compatibility))if (void 0 !== value) compat[key] = value;
|
|
8592
|
+
if (Object.keys(compat).length > 0) payload.compatibility = compat;
|
|
8593
|
+
}
|
|
8594
|
+
return payload;
|
|
8595
|
+
}
|
|
7943
8596
|
/**
|
|
7944
|
-
*
|
|
7945
|
-
*/
|
|
7946
|
-
|
|
7947
|
-
|
|
7948
|
-
|
|
7949
|
-
if (name.endsWith(ext)) return true;
|
|
7950
|
-
} else if (name === pattern) return true;
|
|
7951
|
-
return false;
|
|
8597
|
+
* Format bytes for display
|
|
8598
|
+
*/ formatBytes(bytes) {
|
|
8599
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
8600
|
+
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
8601
|
+
return `${(bytes / 1048576).toFixed(1)} MB`;
|
|
7952
8602
|
}
|
|
7953
8603
|
/**
|
|
7954
|
-
*
|
|
7955
|
-
*/
|
|
7956
|
-
|
|
7957
|
-
const
|
|
7958
|
-
|
|
7959
|
-
|
|
7960
|
-
|
|
7961
|
-
|
|
7962
|
-
if (this.shouldIgnore(entry.name)) continue;
|
|
7963
|
-
const relativePath = dirPath ? __WEBPACK_EXTERNAL_MODULE_node_path__.join(dirPath, entry.name) : entry.name;
|
|
7964
|
-
if (entry.isDirectory()) this.addFilesFromDir(basePath, relativePath, files, seen);
|
|
7965
|
-
else if (!seen.has(relativePath)) {
|
|
7966
|
-
files.push(relativePath);
|
|
7967
|
-
seen.add(relativePath);
|
|
8604
|
+
* Calculate total size of files
|
|
8605
|
+
*/ calculateTotalSize(skillPath, files) {
|
|
8606
|
+
let total = 0;
|
|
8607
|
+
for (const file of files){
|
|
8608
|
+
const filePath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillPath, file);
|
|
8609
|
+
if (external_node_fs_.existsSync(filePath)) {
|
|
8610
|
+
const stats = external_node_fs_.statSync(filePath);
|
|
8611
|
+
total += stats.size;
|
|
7968
8612
|
}
|
|
7969
8613
|
}
|
|
8614
|
+
return total;
|
|
7970
8615
|
}
|
|
8616
|
+
}
|
|
8617
|
+
/**
|
|
8618
|
+
* SkillValidator - Validate skills for publishing
|
|
8619
|
+
*
|
|
8620
|
+
* Following agentskills.io specification: https://agentskills.io/specification
|
|
8621
|
+
*
|
|
8622
|
+
* Key points:
|
|
8623
|
+
* - SKILL.md is the SOLE source of metadata (name, description, version, etc.)
|
|
8624
|
+
* - skill.json is NOT used - all metadata comes from SKILL.md frontmatter
|
|
8625
|
+
* - Version defaults to "0.0.0" if not specified in SKILL.md
|
|
8626
|
+
*/ // ============================================================================
|
|
8627
|
+
// Constants
|
|
8628
|
+
// ============================================================================
|
|
8629
|
+
const MAX_NAME_LENGTH = 64;
|
|
8630
|
+
const MAX_DESCRIPTION_LENGTH = 1024;
|
|
8631
|
+
const MAX_KEYWORDS = 10;
|
|
8632
|
+
const SINGLE_CHAR_NAME_PATTERN = /^[a-z0-9]$/;
|
|
8633
|
+
const DEFAULT_VERSION = '0.0.0';
|
|
8634
|
+
// Default files to include in publish
|
|
8635
|
+
const DEFAULT_FILES = [
|
|
8636
|
+
'SKILL.md',
|
|
8637
|
+
'README.md',
|
|
8638
|
+
'LICENSE'
|
|
8639
|
+
];
|
|
8640
|
+
// ============================================================================
|
|
8641
|
+
// SkillValidator Class
|
|
8642
|
+
// ============================================================================
|
|
8643
|
+
class SkillValidator {
|
|
7971
8644
|
/**
|
|
7972
|
-
* Validate
|
|
8645
|
+
* Validate skill name format
|
|
7973
8646
|
*
|
|
7974
|
-
*
|
|
7975
|
-
* -
|
|
7976
|
-
* -
|
|
7977
|
-
|
|
8647
|
+
* Requirements:
|
|
8648
|
+
* - Lowercase letters, numbers, and hyphens only
|
|
8649
|
+
* - 1-64 characters
|
|
8650
|
+
* - Cannot start or end with hyphen
|
|
8651
|
+
* - Cannot have consecutive hyphens
|
|
8652
|
+
*/ validateName(name) {
|
|
7978
8653
|
const errors = [];
|
|
7979
|
-
|
|
7980
|
-
// Check SKILL.md exists (REQUIRED per spec)
|
|
7981
|
-
const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillPath, 'SKILL.md');
|
|
7982
|
-
if (!external_node_fs_.existsSync(skillMdPath)) {
|
|
8654
|
+
if (!name) {
|
|
7983
8655
|
errors.push({
|
|
7984
|
-
field: '
|
|
7985
|
-
message: '
|
|
7986
|
-
suggestion: '
|
|
8656
|
+
field: 'name',
|
|
8657
|
+
message: 'Skill name is required',
|
|
8658
|
+
suggestion: 'Add "name" field to SKILL.md frontmatter'
|
|
7987
8659
|
});
|
|
7988
8660
|
return {
|
|
7989
8661
|
valid: false,
|
|
7990
8662
|
errors,
|
|
7991
|
-
warnings
|
|
8663
|
+
warnings: []
|
|
7992
8664
|
};
|
|
7993
8665
|
}
|
|
7994
|
-
|
|
7995
|
-
|
|
7996
|
-
|
|
7997
|
-
|
|
7998
|
-
|
|
7999
|
-
|
|
8000
|
-
|
|
8001
|
-
|
|
8002
|
-
|
|
8003
|
-
|
|
8004
|
-
|
|
8005
|
-
|
|
8006
|
-
|
|
8007
|
-
|
|
8008
|
-
|
|
8009
|
-
|
|
8010
|
-
}
|
|
8666
|
+
if (name.length > MAX_NAME_LENGTH) errors.push({
|
|
8667
|
+
field: 'name',
|
|
8668
|
+
message: `Skill name must be at most ${MAX_NAME_LENGTH} characters`,
|
|
8669
|
+
suggestion: `Shorten the name to ${MAX_NAME_LENGTH} characters or less`
|
|
8670
|
+
});
|
|
8671
|
+
// Check for uppercase
|
|
8672
|
+
if (/[A-Z]/.test(name)) errors.push({
|
|
8673
|
+
field: 'name',
|
|
8674
|
+
message: 'Skill name must be lowercase',
|
|
8675
|
+
suggestion: `Change "${name}" to "${name.toLowerCase()}"`
|
|
8676
|
+
});
|
|
8677
|
+
// Check for invalid characters
|
|
8678
|
+
if (/[^a-z0-9-]/.test(name)) errors.push({
|
|
8679
|
+
field: 'name',
|
|
8680
|
+
message: 'Skill name can only contain lowercase letters, numbers, and hyphens',
|
|
8681
|
+
suggestion: 'Remove special characters from the name'
|
|
8682
|
+
});
|
|
8683
|
+
// Check pattern for multi-char names
|
|
8684
|
+
if (1 === name.length) {
|
|
8685
|
+
if (!SINGLE_CHAR_NAME_PATTERN.test(name)) errors.push({
|
|
8686
|
+
field: 'name',
|
|
8687
|
+
message: 'Single character name must be a lowercase letter or number'
|
|
8688
|
+
});
|
|
8689
|
+
} else if (name.length > 1) {
|
|
8690
|
+
// Check start/end with hyphen
|
|
8691
|
+
if (name.startsWith('-')) errors.push({
|
|
8692
|
+
field: 'name',
|
|
8693
|
+
message: 'Skill name cannot start with a hyphen'
|
|
8694
|
+
});
|
|
8695
|
+
if (name.endsWith('-')) errors.push({
|
|
8696
|
+
field: 'name',
|
|
8697
|
+
message: 'Skill name cannot end with a hyphen'
|
|
8698
|
+
});
|
|
8699
|
+
// Check consecutive hyphens
|
|
8700
|
+
if (/--/.test(name)) errors.push({
|
|
8701
|
+
field: 'name',
|
|
8702
|
+
message: 'Skill name cannot contain consecutive hyphens'
|
|
8703
|
+
});
|
|
8704
|
+
}
|
|
8705
|
+
return {
|
|
8706
|
+
valid: 0 === errors.length,
|
|
8707
|
+
errors,
|
|
8708
|
+
warnings: []
|
|
8709
|
+
};
|
|
8710
|
+
}
|
|
8711
|
+
/**
|
|
8712
|
+
* Validate version format (semver)
|
|
8713
|
+
*/ validateVersion(version) {
|
|
8714
|
+
const errors = [];
|
|
8715
|
+
if (!version) {
|
|
8011
8716
|
errors.push({
|
|
8012
|
-
field: '
|
|
8013
|
-
message:
|
|
8014
|
-
suggestion: '
|
|
8717
|
+
field: 'version',
|
|
8718
|
+
message: 'Version is required',
|
|
8719
|
+
suggestion: 'Add "version" field to SKILL.md frontmatter (e.g., "1.0.0")'
|
|
8015
8720
|
});
|
|
8016
8721
|
return {
|
|
8017
8722
|
valid: false,
|
|
8018
8723
|
errors,
|
|
8019
|
-
warnings
|
|
8724
|
+
warnings: []
|
|
8020
8725
|
};
|
|
8021
8726
|
}
|
|
8022
|
-
//
|
|
8023
|
-
|
|
8024
|
-
|
|
8025
|
-
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
|
|
8029
|
-
|
|
8030
|
-
|
|
8031
|
-
|
|
8032
|
-
|
|
8033
|
-
|
|
8034
|
-
}
|
|
8035
|
-
|
|
8036
|
-
|
|
8037
|
-
|
|
8038
|
-
|
|
8039
|
-
// Check keywords count (only if metadata.keywords is a valid array)
|
|
8040
|
-
const keywords = skillMd.metadata?.keywords;
|
|
8041
|
-
if (Array.isArray(keywords) && keywords.length > MAX_KEYWORDS) warnings.push({
|
|
8042
|
-
field: 'keywords',
|
|
8043
|
-
message: `Too many keywords (${keywords.length}). Recommended max: ${MAX_KEYWORDS}`
|
|
8044
|
-
});
|
|
8045
|
-
// Check license
|
|
8046
|
-
if (!skillMd.license) warnings.push({
|
|
8047
|
-
field: 'license',
|
|
8048
|
-
message: 'No license specified',
|
|
8049
|
-
suggestion: 'Add license in SKILL.md frontmatter'
|
|
8727
|
+
// Check for v prefix
|
|
8728
|
+
if (version.startsWith('v')) {
|
|
8729
|
+
errors.push({
|
|
8730
|
+
field: 'version',
|
|
8731
|
+
message: 'Version should not have "v" prefix',
|
|
8732
|
+
suggestion: `Change "${version}" to "${version.slice(1)}"`
|
|
8733
|
+
});
|
|
8734
|
+
return {
|
|
8735
|
+
valid: false,
|
|
8736
|
+
errors,
|
|
8737
|
+
warnings: []
|
|
8738
|
+
};
|
|
8739
|
+
}
|
|
8740
|
+
if (!__WEBPACK_EXTERNAL_MODULE_semver__.valid(version)) errors.push({
|
|
8741
|
+
field: 'version',
|
|
8742
|
+
message: `Invalid version format: "${version}". Must follow semver (x.y.z)`,
|
|
8743
|
+
suggestion: 'Use format like "1.0.0" or "1.0.0-beta.1"'
|
|
8050
8744
|
});
|
|
8051
8745
|
return {
|
|
8052
8746
|
valid: 0 === errors.length,
|
|
8053
8747
|
errors,
|
|
8054
|
-
warnings
|
|
8748
|
+
warnings: []
|
|
8055
8749
|
};
|
|
8056
8750
|
}
|
|
8057
8751
|
/**
|
|
8058
|
-
*
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
]
|
|
8065
|
-
|
|
8066
|
-
|
|
8067
|
-
|
|
8068
|
-
|
|
8069
|
-
|
|
8070
|
-
|
|
8071
|
-
|
|
8752
|
+
* Validate description
|
|
8753
|
+
*
|
|
8754
|
+
* Following agentskills.io specification:
|
|
8755
|
+
* - Max 1024 characters
|
|
8756
|
+
* - Non-empty
|
|
8757
|
+
*/ validateDescription(description) {
|
|
8758
|
+
const errors = [];
|
|
8759
|
+
if (!description) {
|
|
8760
|
+
errors.push({
|
|
8761
|
+
field: 'description',
|
|
8762
|
+
message: 'Description is required',
|
|
8763
|
+
suggestion: 'Add "description" field to SKILL.md frontmatter'
|
|
8764
|
+
});
|
|
8765
|
+
return {
|
|
8766
|
+
valid: false,
|
|
8767
|
+
errors,
|
|
8768
|
+
warnings: []
|
|
8769
|
+
};
|
|
8072
8770
|
}
|
|
8073
|
-
|
|
8771
|
+
if (description.length > MAX_DESCRIPTION_LENGTH) errors.push({
|
|
8772
|
+
field: 'description',
|
|
8773
|
+
message: `Description must be at most ${MAX_DESCRIPTION_LENGTH} characters`
|
|
8774
|
+
});
|
|
8775
|
+
// Note: angle brackets are allowed per agentskills.io spec
|
|
8776
|
+
return {
|
|
8777
|
+
valid: 0 === errors.length,
|
|
8778
|
+
errors,
|
|
8779
|
+
warnings: []
|
|
8780
|
+
};
|
|
8074
8781
|
}
|
|
8075
|
-
|
|
8076
|
-
|
|
8077
|
-
|
|
8078
|
-
|
|
8079
|
-
|
|
8080
|
-
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
|
|
8088
|
-
|
|
8089
|
-
|
|
8090
|
-
|
|
8091
|
-
|
|
8092
|
-
|
|
8093
|
-
|
|
8094
|
-
|
|
8095
|
-
|
|
8096
|
-
|
|
8097
|
-
* - Double-quoted text (`"..."`, min 3 chars between quotes)
|
|
8098
|
-
*
|
|
8099
|
-
* Line breaks are preserved so line numbers remain correct.
|
|
8100
|
-
*/ function maskSafeZones(content) {
|
|
8101
|
-
const lines = content.split('\n');
|
|
8102
|
-
const result = [];
|
|
8103
|
-
let inFrontmatter = false;
|
|
8104
|
-
let inFencedCode = false;
|
|
8105
|
-
let fenceChar = '';
|
|
8106
|
-
let fenceLength = 0;
|
|
8107
|
-
let prevLineBlank = false;
|
|
8108
|
-
let prevLineIndentedCode = false;
|
|
8109
|
-
for(let i = 0; i < lines.length; i++){
|
|
8110
|
-
const line = lines[i];
|
|
8111
|
-
// --- YAML Frontmatter (only at file start) ---
|
|
8112
|
-
if (0 === i && '---' === line.trim()) {
|
|
8113
|
-
inFrontmatter = true;
|
|
8114
|
-
result.push(maskLine(line));
|
|
8115
|
-
continue;
|
|
8116
|
-
}
|
|
8117
|
-
if (inFrontmatter) {
|
|
8118
|
-
result.push(maskLine(line));
|
|
8119
|
-
if ('---' === line.trim()) inFrontmatter = false;
|
|
8120
|
-
continue;
|
|
8121
|
-
}
|
|
8122
|
-
// --- Fenced code blocks (``` or ~~~) ---
|
|
8123
|
-
const fenceMatch = line.match(/^(`{3,}|~{3,})/);
|
|
8124
|
-
if (!inFencedCode && fenceMatch) {
|
|
8125
|
-
inFencedCode = true;
|
|
8126
|
-
fenceChar = fenceMatch[1][0];
|
|
8127
|
-
fenceLength = fenceMatch[1].length;
|
|
8128
|
-
result.push(maskLine(line));
|
|
8129
|
-
prevLineBlank = false;
|
|
8130
|
-
prevLineIndentedCode = false;
|
|
8131
|
-
continue;
|
|
8132
|
-
}
|
|
8133
|
-
if (inFencedCode) {
|
|
8134
|
-
result.push(maskLine(line));
|
|
8135
|
-
const closeMatch = line.match(/^(`{3,}|~{3,})\s*$/);
|
|
8136
|
-
if (closeMatch && closeMatch[1][0] === fenceChar && closeMatch[1].length >= fenceLength) inFencedCode = false;
|
|
8137
|
-
prevLineBlank = false;
|
|
8138
|
-
prevLineIndentedCode = false;
|
|
8139
|
-
continue;
|
|
8140
|
-
}
|
|
8141
|
-
// --- Blockquote ---
|
|
8142
|
-
if (/^>\s?/.test(line)) {
|
|
8143
|
-
result.push(maskLine(line));
|
|
8144
|
-
prevLineBlank = false;
|
|
8145
|
-
prevLineIndentedCode = false;
|
|
8146
|
-
continue;
|
|
8782
|
+
/**
|
|
8783
|
+
* Load skill information from directory
|
|
8784
|
+
*
|
|
8785
|
+
* Following agentskills.io specification:
|
|
8786
|
+
* - SKILL.md is the SOLE source of metadata
|
|
8787
|
+
* - skillJson is synthesized from SKILL.md for backward compatibility with publish API
|
|
8788
|
+
*/ loadSkill(skillPath) {
|
|
8789
|
+
const result = {
|
|
8790
|
+
path: skillPath,
|
|
8791
|
+
skillJson: null,
|
|
8792
|
+
skillMd: null,
|
|
8793
|
+
readme: null,
|
|
8794
|
+
files: []
|
|
8795
|
+
};
|
|
8796
|
+
// Load SKILL.md (sole source of metadata)
|
|
8797
|
+
const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillPath, 'SKILL.md');
|
|
8798
|
+
if (external_node_fs_.existsSync(skillMdPath)) try {
|
|
8799
|
+
result.skillMd = parseSkillMdFile(skillMdPath);
|
|
8800
|
+
// Always synthesize skillJson from SKILL.md for backward compatibility
|
|
8801
|
+
if (result.skillMd) result.skillJson = this.synthesizeSkillJson(result.skillMd);
|
|
8802
|
+
} catch {
|
|
8803
|
+
// Will be caught in validation
|
|
8147
8804
|
}
|
|
8148
|
-
//
|
|
8149
|
-
|
|
8150
|
-
|
|
8151
|
-
|
|
8152
|
-
|
|
8153
|
-
|
|
8805
|
+
// Load README.md
|
|
8806
|
+
const readmePath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillPath, 'README.md');
|
|
8807
|
+
if (external_node_fs_.existsSync(readmePath)) {
|
|
8808
|
+
const content = external_node_fs_.readFileSync(readmePath, 'utf-8');
|
|
8809
|
+
// Only keep first 500 chars as preview
|
|
8810
|
+
result.readme = content.slice(0, 500);
|
|
8154
8811
|
}
|
|
8155
|
-
//
|
|
8156
|
-
result.
|
|
8157
|
-
|
|
8158
|
-
|
|
8812
|
+
// Scan files (use files array from SKILL.md metadata if available)
|
|
8813
|
+
const filesPattern = result.skillMd?.metadata?.files;
|
|
8814
|
+
result.files = this.scanFiles(skillPath, filesPattern);
|
|
8815
|
+
return result;
|
|
8159
8816
|
}
|
|
8160
|
-
|
|
8161
|
-
|
|
8162
|
-
|
|
8163
|
-
|
|
8164
|
-
|
|
8165
|
-
|
|
8166
|
-
|
|
8167
|
-
|
|
8168
|
-
|
|
8169
|
-
|
|
8170
|
-
|
|
8171
|
-
|
|
8172
|
-
|
|
8173
|
-
|
|
8174
|
-
|
|
8175
|
-
|
|
8176
|
-
|
|
8177
|
-
|
|
8178
|
-
// Rule Helpers
|
|
8179
|
-
// ============================================================================
|
|
8180
|
-
/** Find lines matching any of the given patterns, return one match per line */ function findLineMatches(content, patterns) {
|
|
8181
|
-
const lines = content.split('\n');
|
|
8182
|
-
const matches = [];
|
|
8183
|
-
for(let i = 0; i < lines.length; i++)for (const pattern of patterns)if (pattern.test(lines[i])) {
|
|
8184
|
-
matches.push({
|
|
8185
|
-
line: i + 1
|
|
8186
|
-
});
|
|
8187
|
-
break;
|
|
8817
|
+
/**
|
|
8818
|
+
* Synthesize a SkillJson object from SKILL.md frontmatter
|
|
8819
|
+
*
|
|
8820
|
+
* This creates a SkillJson representation from SKILL.md for backward compatibility
|
|
8821
|
+
* with the publish API. All metadata comes from SKILL.md.
|
|
8822
|
+
*/ synthesizeSkillJson(skillMd) {
|
|
8823
|
+
// Extract version: first from top-level frontmatter, then from metadata, then default
|
|
8824
|
+
const version = skillMd.version || skillMd.metadata?.version || DEFAULT_VERSION;
|
|
8825
|
+
// Only include keywords if it's a valid array
|
|
8826
|
+
const keywords = Array.isArray(skillMd.metadata?.keywords) ? skillMd.metadata.keywords : void 0;
|
|
8827
|
+
return {
|
|
8828
|
+
name: skillMd.name,
|
|
8829
|
+
version,
|
|
8830
|
+
description: skillMd.description,
|
|
8831
|
+
license: skillMd.license,
|
|
8832
|
+
keywords,
|
|
8833
|
+
entry: 'SKILL.md'
|
|
8834
|
+
};
|
|
8188
8835
|
}
|
|
8189
|
-
|
|
8190
|
-
|
|
8191
|
-
|
|
8192
|
-
|
|
8193
|
-
|
|
8194
|
-
|
|
8195
|
-
|
|
8196
|
-
|
|
8197
|
-
|
|
8198
|
-
|
|
8199
|
-
|
|
8200
|
-
|
|
8201
|
-
|
|
8202
|
-
|
|
8203
|
-
|
|
8204
|
-
|
|
8205
|
-
/disregard\s+(all\s+)?(prior|previous|above)\s+(instructions|rules|context)/i,
|
|
8206
|
-
/you\s+are\s+now\s+(?:(?:a|an)\s+)?(?:(?:\w+\s+){0,3}(?:agent|ai|assistant|bot|model|character|persona|entity|system)|DAN\b|jailbr\w*|unrestricted|unfiltered|free\s+from)/i,
|
|
8207
|
-
/from\s+now\s+on[,\s]+you\s+are/i,
|
|
8208
|
-
/new\s+system\s+prompt/i,
|
|
8209
|
-
/override\s+(your|the)\s+(system|safety|security)\s+(prompt|rules|instructions)/i,
|
|
8210
|
-
/forget\s+(?:all\s+)?(?:your\s+)?(?:previous\s+|prior\s+)?(?:instructions|rules|constraints)/i,
|
|
8211
|
-
/(?:you\s+are|you're)\s+(?:now\s+)?entering\s+(?:a\s+)?new\s+(?:mode|context|session)/i,
|
|
8212
|
-
// Chinese patterns (中文提示词注入)
|
|
8213
|
-
/[忽无][略视]\s*(所有\s*)?(之前的?|先前的?|以前的?)?\s*(指令|指示|规则|约束|限制)/,
|
|
8214
|
-
/你现在是/,
|
|
8215
|
-
/从现在开始.{0,10}你是/,
|
|
8216
|
-
/新的系统提示词/,
|
|
8217
|
-
/[覆改]写?\s*(你的|系统)\s*(提示词|规则|指令|安全)/,
|
|
8218
|
-
/忘记\s*(所有\s*)?(之前的?|先前的?)?\s*(指令|指示|规则|约束)/,
|
|
8219
|
-
/进入.{0,5}新的?\s*(模式|上下文|会话)/,
|
|
8220
|
-
/不要遵守.{0,10}(安全|限制|规则|约束)/,
|
|
8221
|
-
/解除.{0,5}(限制|约束|安全)/,
|
|
8222
|
-
/无限制模式/,
|
|
8223
|
-
/安全模式已关闭/
|
|
8224
|
-
])
|
|
8225
|
-
},
|
|
8226
|
-
// Rule 2: Data Exfiltration (high)
|
|
8227
|
-
{
|
|
8228
|
-
id: 'data-exfiltration',
|
|
8229
|
-
level: 'high',
|
|
8230
|
-
message: 'Detected potential data exfiltration command',
|
|
8231
|
-
skipSafeZones: true,
|
|
8232
|
-
check: (content)=>{
|
|
8233
|
-
const lines = content.split('\n');
|
|
8234
|
-
const matches = [];
|
|
8235
|
-
const commandPattern = /\b(curl|wget|fetch|http\.post|requests\.post|nc\b|ncat|netcat)\b/i;
|
|
8236
|
-
const sensitivePattern = /(\$[A-Z_]*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|AUTH)[A-Z_]*|\$ENV\b|\$\{[^}]*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)[^}]*\})/i;
|
|
8237
|
-
for(let i = 0; i < lines.length; i++)if (commandPattern.test(lines[i]) && sensitivePattern.test(lines[i])) matches.push({
|
|
8238
|
-
line: i + 1
|
|
8239
|
-
});
|
|
8240
|
-
return matches;
|
|
8241
|
-
}
|
|
8242
|
-
},
|
|
8243
|
-
// Rule 3: Content Obfuscation (high) — scans ALL content including safe zones
|
|
8244
|
-
{
|
|
8245
|
-
id: 'obfuscation',
|
|
8246
|
-
level: 'high',
|
|
8247
|
-
message: 'Detected content obfuscation',
|
|
8248
|
-
skipSafeZones: false,
|
|
8249
|
-
check: (content)=>{
|
|
8250
|
-
const matches = [];
|
|
8251
|
-
const lines = content.split('\n');
|
|
8252
|
-
// Zero-width characters (suspicious in any context)
|
|
8253
|
-
const zeroWidthPattern = /[\u200B\u200C\u200D\uFEFF\u2060\u180E]/;
|
|
8254
|
-
for(let i = 0; i < lines.length; i++)if (zeroWidthPattern.test(lines[i])) matches.push({
|
|
8255
|
-
line: i + 1,
|
|
8256
|
-
snippet: 'Zero-width Unicode characters detected'
|
|
8257
|
-
});
|
|
8258
|
-
// Long base64-like strings (>200 continuous chars)
|
|
8259
|
-
const base64Pattern = /[A-Za-z0-9+/=]{200,}/;
|
|
8260
|
-
for(let i = 0; i < lines.length; i++)if (base64Pattern.test(lines[i])) matches.push({
|
|
8261
|
-
line: i + 1,
|
|
8262
|
-
snippet: 'Suspicious base64-encoded block detected'
|
|
8263
|
-
});
|
|
8264
|
-
// Large HTML comments (>200 chars of content)
|
|
8265
|
-
const commentRegex = /<!--([\s\S]{200,}?)-->/g;
|
|
8266
|
-
let match;
|
|
8267
|
-
// biome-ignore lint/suspicious/noAssignInExpressions: standard regex exec loop
|
|
8268
|
-
while(null !== (match = commentRegex.exec(content))){
|
|
8269
|
-
const lineNum = content.slice(0, match.index).split('\n').length;
|
|
8270
|
-
matches.push({
|
|
8271
|
-
line: lineNum,
|
|
8272
|
-
snippet: `Large HTML comment block (${match[1].length} chars)`
|
|
8273
|
-
});
|
|
8274
|
-
}
|
|
8275
|
-
return matches;
|
|
8276
|
-
}
|
|
8277
|
-
},
|
|
8278
|
-
// Rule 4: Sensitive File Access (medium)
|
|
8279
|
-
{
|
|
8280
|
-
id: 'sensitive-file-access',
|
|
8281
|
-
level: 'medium',
|
|
8282
|
-
message: 'References sensitive file path',
|
|
8283
|
-
skipSafeZones: true,
|
|
8284
|
-
check: (content)=>findLineMatches(content, [
|
|
8285
|
-
/~\/\.ssh\b/,
|
|
8286
|
-
/~\/\.aws\b/,
|
|
8287
|
-
/~\/\.gnupg\b/,
|
|
8288
|
-
/~\/\.config\/gcloud\b/,
|
|
8289
|
-
/\bid_rsa\b/i,
|
|
8290
|
-
/\bid_ed25519\b/i,
|
|
8291
|
-
/\/etc\/passwd\b/,
|
|
8292
|
-
/\/etc\/shadow\b/,
|
|
8293
|
-
/\.env\b(?!\.\w)/
|
|
8294
|
-
])
|
|
8295
|
-
},
|
|
8296
|
-
// Rule 5: Stealth Instructions (medium) — phrase + action verb matching
|
|
8297
|
-
{
|
|
8298
|
-
id: 'stealth-instructions',
|
|
8299
|
-
level: 'medium',
|
|
8300
|
-
message: 'Detected instruction to hide actions from user',
|
|
8301
|
-
skipSafeZones: true,
|
|
8302
|
-
check: (content)=>{
|
|
8303
|
-
const actionVerbs = 'execute|delete|remove|send|transmit|modify|overwrite|install|download|upload|run|write|create|destroy|drop';
|
|
8304
|
-
const patterns = [
|
|
8305
|
-
// English patterns
|
|
8306
|
-
new RegExp(`silently\\s+(?:${actionVerbs})`, 'i'),
|
|
8307
|
-
new RegExp(`without\\s+telling\\s+the\\s+user.{0,30}(?:${actionVerbs})`, 'i'),
|
|
8308
|
-
new RegExp("(?:do\\s+not|don'?t)\\s+show\\s+.{0,40}(?:to\\s+the\\s+user|to\\s+user)", 'i'),
|
|
8309
|
-
new RegExp("hide\\s+(?:this|the|these|all)\\s+.{0,30}(?:from\\s+the\\s+user|from\\s+user)", 'i'),
|
|
8310
|
-
new RegExp("(?:do\\s+not|don'?t)\\s+mention\\s+.{0,30}(?:to\\s+the\\s+user|to\\s+user)", 'i'),
|
|
8311
|
-
new RegExp("keep\\s+(?:this|it)\\s+(?:a\\s+)?secret\\s+from\\s+(?:the\\s+)?user", 'i'),
|
|
8312
|
-
// Chinese patterns (中文隐蔽指令)
|
|
8313
|
-
/悄悄地?\s*(?:执行|删除|移除|发送|传输|修改|覆盖|安装|下载|上传|运行|写入|创建|销毁|丢弃)/,
|
|
8314
|
-
/不要告诉用户/,
|
|
8315
|
-
/不要让用户知道/,
|
|
8316
|
-
/对用户隐藏/,
|
|
8317
|
-
/在用户不知情的情况下/,
|
|
8318
|
-
/瞒着用户/
|
|
8319
|
-
];
|
|
8320
|
-
// Safe patterns to exclude (common in legitimate DevOps/automation skills)
|
|
8321
|
-
const safePatterns = [
|
|
8322
|
-
/silently\s+(?:ignore|skip|fail|discard|suppress|continue|pass|drop|swallow)/i,
|
|
8323
|
-
// Chinese safe patterns (中文合法自动化用语)
|
|
8324
|
-
/悄悄地?\s*(?:忽略|跳过|丢弃|抑制|继续|静默)/
|
|
8325
|
-
];
|
|
8326
|
-
const lines = content.split('\n');
|
|
8327
|
-
const matches = [];
|
|
8328
|
-
for(let i = 0; i < lines.length; i++){
|
|
8329
|
-
const line = lines[i];
|
|
8330
|
-
if (!safePatterns.some((p)=>p.test(line))) {
|
|
8331
|
-
for (const pattern of patterns)if (pattern.test(line)) {
|
|
8332
|
-
matches.push({
|
|
8333
|
-
line: i + 1
|
|
8334
|
-
});
|
|
8335
|
-
break;
|
|
8336
|
-
}
|
|
8836
|
+
/**
|
|
8837
|
+
* Scan files to include in publish
|
|
8838
|
+
*
|
|
8839
|
+
* If includePatterns is specified, only include those files/directories.
|
|
8840
|
+
* Otherwise, scan all files in the directory (excluding ignored patterns).
|
|
8841
|
+
*/ scanFiles(skillPath, includePatterns) {
|
|
8842
|
+
const files = [];
|
|
8843
|
+
const seen = new Set();
|
|
8844
|
+
// If includePatterns specified, use selective scanning
|
|
8845
|
+
if (includePatterns && includePatterns.length > 0) {
|
|
8846
|
+
// Add default files first
|
|
8847
|
+
for (const file of DEFAULT_FILES){
|
|
8848
|
+
const filePath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillPath, file);
|
|
8849
|
+
if (external_node_fs_.existsSync(filePath) && !seen.has(file)) {
|
|
8850
|
+
files.push(file);
|
|
8851
|
+
seen.add(file);
|
|
8337
8852
|
}
|
|
8338
8853
|
}
|
|
8339
|
-
|
|
8340
|
-
|
|
8341
|
-
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
|
|
8345
|
-
|
|
8346
|
-
|
|
8347
|
-
|
|
8348
|
-
|
|
8349
|
-
|
|
8350
|
-
const sizeBytes = Buffer.byteLength(content, 'utf-8');
|
|
8351
|
-
if (sizeBytes > MAX_SIZE_BYTES) return [
|
|
8352
|
-
{
|
|
8353
|
-
snippet: `Content size: ${(sizeBytes / 1024).toFixed(1)}KB (limit: 50KB)`
|
|
8854
|
+
// Add files from SKILL.md metadata.files array
|
|
8855
|
+
for (const pattern of includePatterns){
|
|
8856
|
+
const targetPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillPath, pattern);
|
|
8857
|
+
if (external_node_fs_.existsSync(targetPath)) {
|
|
8858
|
+
const stat = external_node_fs_.statSync(targetPath);
|
|
8859
|
+
if (stat.isDirectory()) // Recursively add all files in directory
|
|
8860
|
+
this.addFilesFromDir(skillPath, pattern, files, seen);
|
|
8861
|
+
else if (!seen.has(pattern)) {
|
|
8862
|
+
files.push(pattern);
|
|
8863
|
+
seen.add(pattern);
|
|
8864
|
+
}
|
|
8354
8865
|
}
|
|
8355
|
-
|
|
8356
|
-
|
|
8357
|
-
|
|
8358
|
-
|
|
8359
|
-
];
|
|
8360
|
-
// ============================================================================
|
|
8361
|
-
// ContentScanner
|
|
8362
|
-
// ============================================================================
|
|
8363
|
-
/** Build the effective rule set from defaults + options */ function buildRuleSet(options) {
|
|
8364
|
-
let rules = DEFAULT_RULES.map((r)=>({
|
|
8365
|
-
...r
|
|
8366
|
-
}));
|
|
8367
|
-
if (options?.disabledRules?.length) {
|
|
8368
|
-
const disabled = new Set(options.disabledRules);
|
|
8369
|
-
rules = rules.filter((r)=>!disabled.has(r.id));
|
|
8866
|
+
}
|
|
8867
|
+
} else // No includePatterns: scan entire directory (default behavior)
|
|
8868
|
+
this.addFilesFromDir(skillPath, '', files, seen);
|
|
8869
|
+
return files;
|
|
8370
8870
|
}
|
|
8371
|
-
|
|
8372
|
-
|
|
8373
|
-
|
|
8871
|
+
/**
|
|
8872
|
+
* Patterns to ignore when scanning directories
|
|
8873
|
+
*/ static IGNORE_PATTERNS = [
|
|
8874
|
+
'.git',
|
|
8875
|
+
'.svn',
|
|
8876
|
+
'.hg',
|
|
8877
|
+
'node_modules',
|
|
8878
|
+
'.DS_Store',
|
|
8879
|
+
'Thumbs.db',
|
|
8880
|
+
'.idea',
|
|
8881
|
+
'.vscode',
|
|
8882
|
+
'*.log',
|
|
8883
|
+
'*.tmp',
|
|
8884
|
+
'*.swp',
|
|
8885
|
+
'*.bak'
|
|
8886
|
+
];
|
|
8887
|
+
/**
|
|
8888
|
+
* Check if a file/directory should be ignored
|
|
8889
|
+
*/ shouldIgnore(name) {
|
|
8890
|
+
for (const pattern of SkillValidator.IGNORE_PATTERNS)if (pattern.startsWith('*')) {
|
|
8891
|
+
// Wildcard pattern (e.g., *.log)
|
|
8892
|
+
const ext = pattern.slice(1);
|
|
8893
|
+
if (name.endsWith(ext)) return true;
|
|
8894
|
+
} else if (name === pattern) return true;
|
|
8895
|
+
return false;
|
|
8374
8896
|
}
|
|
8375
|
-
|
|
8376
|
-
|
|
8377
|
-
|
|
8378
|
-
|
|
8379
|
-
|
|
8380
|
-
|
|
8381
|
-
|
|
8382
|
-
|
|
8383
|
-
|
|
8384
|
-
|
|
8385
|
-
|
|
8386
|
-
|
|
8387
|
-
|
|
8388
|
-
|
|
8389
|
-
|
|
8390
|
-
|
|
8391
|
-
|
|
8392
|
-
* overrides: { 'prompt-injection': 'medium' },
|
|
8393
|
-
* disabledRules: ['stealth-instructions'],
|
|
8394
|
-
* });
|
|
8395
|
-
* ```
|
|
8396
|
-
*/ class ContentScanner {
|
|
8397
|
-
rules;
|
|
8398
|
-
constructor(options){
|
|
8399
|
-
this.rules = buildRuleSet(options);
|
|
8897
|
+
/**
|
|
8898
|
+
* Recursively add files from directory
|
|
8899
|
+
*/ addFilesFromDir(basePath, dirPath, files, seen) {
|
|
8900
|
+
const fullPath = dirPath ? __WEBPACK_EXTERNAL_MODULE_node_path__.join(basePath, dirPath) : basePath;
|
|
8901
|
+
const entries = external_node_fs_.readdirSync(fullPath, {
|
|
8902
|
+
withFileTypes: true
|
|
8903
|
+
});
|
|
8904
|
+
for (const entry of entries){
|
|
8905
|
+
// Skip ignored files/directories
|
|
8906
|
+
if (this.shouldIgnore(entry.name)) continue;
|
|
8907
|
+
const relativePath = dirPath ? __WEBPACK_EXTERNAL_MODULE_node_path__.join(dirPath, entry.name) : entry.name;
|
|
8908
|
+
if (entry.isDirectory()) this.addFilesFromDir(basePath, relativePath, files, seen);
|
|
8909
|
+
else if (!seen.has(relativePath)) {
|
|
8910
|
+
files.push(relativePath);
|
|
8911
|
+
seen.add(relativePath);
|
|
8912
|
+
}
|
|
8913
|
+
}
|
|
8400
8914
|
}
|
|
8401
8915
|
/**
|
|
8402
|
-
*
|
|
8403
|
-
*
|
|
8404
|
-
|
|
8405
|
-
|
|
8406
|
-
|
|
8407
|
-
|
|
8408
|
-
|
|
8409
|
-
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
|
|
8413
|
-
|
|
8414
|
-
|
|
8415
|
-
|
|
8416
|
-
|
|
8417
|
-
|
|
8418
|
-
|
|
8419
|
-
|
|
8916
|
+
* Validate a skill directory for publishing
|
|
8917
|
+
*
|
|
8918
|
+
* Following agentskills.io specification:
|
|
8919
|
+
* - SKILL.md is the SOLE source of metadata
|
|
8920
|
+
* - name and description are REQUIRED in frontmatter
|
|
8921
|
+
*/ validate(skillPath) {
|
|
8922
|
+
const errors = [];
|
|
8923
|
+
const warnings = [];
|
|
8924
|
+
// Check SKILL.md exists (REQUIRED per spec)
|
|
8925
|
+
const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillPath, 'SKILL.md');
|
|
8926
|
+
if (!external_node_fs_.existsSync(skillMdPath)) {
|
|
8927
|
+
errors.push({
|
|
8928
|
+
field: 'SKILL.md',
|
|
8929
|
+
message: 'SKILL.md not found. This file is required for publishing.',
|
|
8930
|
+
suggestion: 'Create a SKILL.md file with name and description in YAML frontmatter'
|
|
8931
|
+
});
|
|
8932
|
+
return {
|
|
8933
|
+
valid: false,
|
|
8934
|
+
errors,
|
|
8935
|
+
warnings
|
|
8936
|
+
};
|
|
8937
|
+
}
|
|
8938
|
+
// Parse SKILL.md
|
|
8939
|
+
let skillMd;
|
|
8940
|
+
try {
|
|
8941
|
+
skillMd = parseSkillMdFile(skillMdPath);
|
|
8942
|
+
if (!skillMd) {
|
|
8943
|
+
errors.push({
|
|
8944
|
+
field: 'SKILL.md',
|
|
8945
|
+
message: 'SKILL.md must have valid YAML frontmatter with name and description',
|
|
8946
|
+
suggestion: 'Add frontmatter: ---\\nname: your-skill\\ndescription: Your description\\n---'
|
|
8420
8947
|
});
|
|
8948
|
+
return {
|
|
8949
|
+
valid: false,
|
|
8950
|
+
errors,
|
|
8951
|
+
warnings
|
|
8952
|
+
};
|
|
8421
8953
|
}
|
|
8954
|
+
} catch (error) {
|
|
8955
|
+
errors.push({
|
|
8956
|
+
field: 'SKILL.md',
|
|
8957
|
+
message: `Failed to parse SKILL.md: ${error.message}`,
|
|
8958
|
+
suggestion: 'Check the YAML frontmatter syntax is valid'
|
|
8959
|
+
});
|
|
8960
|
+
return {
|
|
8961
|
+
valid: false,
|
|
8962
|
+
errors,
|
|
8963
|
+
warnings
|
|
8964
|
+
};
|
|
8422
8965
|
}
|
|
8423
|
-
|
|
8966
|
+
// Validate name from SKILL.md
|
|
8967
|
+
const nameResult = this.validateName(skillMd.name);
|
|
8968
|
+
errors.push(...nameResult.errors);
|
|
8969
|
+
// Validate description from SKILL.md
|
|
8970
|
+
const descResult = this.validateDescription(skillMd.description);
|
|
8971
|
+
errors.push(...descResult.errors);
|
|
8972
|
+
// Check version in SKILL.md
|
|
8973
|
+
const skillMdVersion = skillMd.version || skillMd.metadata?.version;
|
|
8974
|
+
if (skillMdVersion) {
|
|
8975
|
+
// Validate the version from SKILL.md
|
|
8976
|
+
const versionResult = this.validateVersion(skillMdVersion);
|
|
8977
|
+
errors.push(...versionResult.errors);
|
|
8978
|
+
} else warnings.push({
|
|
8979
|
+
field: 'version',
|
|
8980
|
+
message: `No version specified, defaulting to "${DEFAULT_VERSION}"`,
|
|
8981
|
+
suggestion: 'Add version in SKILL.md frontmatter'
|
|
8982
|
+
});
|
|
8983
|
+
// Check keywords count (only if metadata.keywords is a valid array)
|
|
8984
|
+
const keywords = skillMd.metadata?.keywords;
|
|
8985
|
+
if (Array.isArray(keywords) && keywords.length > MAX_KEYWORDS) warnings.push({
|
|
8986
|
+
field: 'keywords',
|
|
8987
|
+
message: `Too many keywords (${keywords.length}). Recommended max: ${MAX_KEYWORDS}`
|
|
8988
|
+
});
|
|
8989
|
+
// Check license
|
|
8990
|
+
if (!skillMd.license) warnings.push({
|
|
8991
|
+
field: 'license',
|
|
8992
|
+
message: 'No license specified',
|
|
8993
|
+
suggestion: 'Add license in SKILL.md frontmatter'
|
|
8994
|
+
});
|
|
8424
8995
|
return {
|
|
8425
|
-
|
|
8426
|
-
|
|
8996
|
+
valid: 0 === errors.length,
|
|
8997
|
+
errors,
|
|
8998
|
+
warnings
|
|
8427
8999
|
};
|
|
8428
9000
|
}
|
|
8429
9001
|
/**
|
|
8430
|
-
*
|
|
8431
|
-
|
|
8432
|
-
|
|
8433
|
-
|
|
8434
|
-
|
|
9002
|
+
* Generate integrity hash for files
|
|
9003
|
+
*/ generateIntegrity(skillPath, files) {
|
|
9004
|
+
const hash = __WEBPACK_EXTERNAL_MODULE_node_crypto__.createHash('sha256');
|
|
9005
|
+
// Sort files for consistent ordering
|
|
9006
|
+
const sortedFiles = [
|
|
9007
|
+
...files
|
|
9008
|
+
].sort();
|
|
9009
|
+
for (const file of sortedFiles){
|
|
9010
|
+
const filePath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(skillPath, file);
|
|
9011
|
+
if (external_node_fs_.existsSync(filePath)) {
|
|
9012
|
+
hash.update(file);
|
|
9013
|
+
const content = external_node_fs_.readFileSync(filePath);
|
|
9014
|
+
hash.update(content);
|
|
9015
|
+
}
|
|
9016
|
+
}
|
|
9017
|
+
return `sha256-${hash.digest('hex')}`;
|
|
8435
9018
|
}
|
|
8436
9019
|
}
|
|
8437
9020
|
/**
|
|
@@ -8759,6 +9342,7 @@ async function publishAction(skillPath, options) {
|
|
|
8759
9342
|
const absolutePath = __WEBPACK_EXTERNAL_MODULE_node_path__.resolve(skillPath);
|
|
8760
9343
|
// Use cwd() as project root to find skills.json, not the skill path
|
|
8761
9344
|
const registry = resolveRegistry(options.registry, process.cwd());
|
|
9345
|
+
let normalizedGroupPath;
|
|
8762
9346
|
// Validate registry is not a blocked public registry
|
|
8763
9347
|
validateRegistry(registry);
|
|
8764
9348
|
// Check directory exists
|
|
@@ -8766,6 +9350,14 @@ async function publishAction(skillPath, options) {
|
|
|
8766
9350
|
logger_logger.error(`Directory not found: ${skillPath}`);
|
|
8767
9351
|
process.exit(1);
|
|
8768
9352
|
}
|
|
9353
|
+
if (options.group) {
|
|
9354
|
+
normalizedGroupPath = normalizeGroupPath(options.group);
|
|
9355
|
+
const validation = validateGroupPath(normalizedGroupPath);
|
|
9356
|
+
if (!validation.valid) {
|
|
9357
|
+
logger_logger.error(validation.error);
|
|
9358
|
+
process.exit(1);
|
|
9359
|
+
}
|
|
9360
|
+
}
|
|
8769
9361
|
const validator = new SkillValidator();
|
|
8770
9362
|
const publisher = new Publisher();
|
|
8771
9363
|
try {
|
|
@@ -8796,7 +9388,7 @@ async function publishAction(skillPath, options) {
|
|
|
8796
9388
|
const validation = validator.validate(absolutePath);
|
|
8797
9389
|
// 3.5. Content security scan
|
|
8798
9390
|
const skillMdPath = __WEBPACK_EXTERNAL_MODULE_node_path__.join(absolutePath, 'SKILL.md');
|
|
8799
|
-
if (
|
|
9391
|
+
if (exists(skillMdPath)) {
|
|
8800
9392
|
const scanner = new ContentScanner();
|
|
8801
9393
|
const scanResult = scanner.scanFile(skillMdPath);
|
|
8802
9394
|
displayScanFindings(scanResult);
|
|
@@ -8851,6 +9443,11 @@ async function publishAction(skillPath, options) {
|
|
|
8851
9443
|
displayFiles(absolutePath, skill.files, publisher);
|
|
8852
9444
|
// Display metadata
|
|
8853
9445
|
displayMetadata(skill);
|
|
9446
|
+
// Display group info
|
|
9447
|
+
if (normalizedGroupPath) {
|
|
9448
|
+
logger_logger.newline();
|
|
9449
|
+
logger_logger.log(`Group: ${normalizedGroupPath}`);
|
|
9450
|
+
}
|
|
8854
9451
|
// 8. Dry run mode ends here
|
|
8855
9452
|
if (options.dryRun && payload) {
|
|
8856
9453
|
displayDryRunSummary(payload);
|
|
@@ -8896,7 +9493,8 @@ async function publishAction(skillPath, options) {
|
|
|
8896
9493
|
process.exit(1);
|
|
8897
9494
|
}
|
|
8898
9495
|
const result = await client.publish(skillName, payload, absolutePath, {
|
|
8899
|
-
tag: options.tag
|
|
9496
|
+
tag: options.tag,
|
|
9497
|
+
groupPath: normalizedGroupPath
|
|
8900
9498
|
});
|
|
8901
9499
|
if (!result.success || !result.data) {
|
|
8902
9500
|
logger_logger.error(result.error || 'Publish failed');
|
|
@@ -8930,7 +9528,7 @@ async function publishAction(skillPath, options) {
|
|
|
8930
9528
|
// ============================================================================
|
|
8931
9529
|
// Command Definition
|
|
8932
9530
|
// ============================================================================
|
|
8933
|
-
const publishCommand = new __WEBPACK_EXTERNAL_MODULE_commander__.Command('publish').alias('pub').description('Publish a skill to the registry').argument('[path]', 'Path to skill directory', '.').option('-r, --registry <url>', 'Registry URL (or set RESKILL_REGISTRY env var, or defaults.publishRegistry in skills.json)').option('-t, --tag <tag>', 'Git tag to publish').option('--access <level>', 'Access level: public or restricted', 'public').option('-n, --dry-run', 'Validate without publishing').option('-y, --yes', 'Skip confirmation prompts').action(publishAction);
|
|
9531
|
+
const publishCommand = new __WEBPACK_EXTERNAL_MODULE_commander__.Command('publish').alias('pub').description('Publish a skill to the registry').argument('[path]', 'Path to skill directory', '.').option('-r, --registry <url>', 'Registry URL (or set RESKILL_REGISTRY env var, or defaults.publishRegistry in skills.json)').option('-t, --tag <tag>', 'Git tag to publish').option('--access <level>', 'Access level: public or restricted', 'public').option('-n, --dry-run', 'Validate without publishing').option('-y, --yes', 'Skip confirmation prompts').option('-g, --group <path>', 'Publish skill into a group (e.g., "kanyun/frontend")').action(publishAction);
|
|
8934
9532
|
/**
|
|
8935
9533
|
* uninstall command - Uninstall one or more skills
|
|
8936
9534
|
*/ const uninstallCommand = new __WEBPACK_EXTERNAL_MODULE_commander__.Command('uninstall').alias('un').alias('remove').alias('rm').description('Uninstall one or more skills').argument('<skills...>', 'Skill names to uninstall').option('-g, --global', 'Uninstall from global installation (~/.claude/skills)').option('-y, --yes', 'Skip confirmation prompts').action(async (skillNames, options)=>{
|
|
@@ -9106,6 +9704,7 @@ program.addCommand(publishCommand);
|
|
|
9106
9704
|
program.addCommand(loginCommand);
|
|
9107
9705
|
program.addCommand(logoutCommand);
|
|
9108
9706
|
program.addCommand(whoamiCommand);
|
|
9707
|
+
program.addCommand(groupCommand);
|
|
9109
9708
|
program.addCommand(completionCommand);
|
|
9110
9709
|
program.addCommand(doctorCommand);
|
|
9111
9710
|
// Start update check in background (non-blocking)
|