skillscokac 1.1.0 → 1.3.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/bin/skillscokac.js +141 -74
- package/package.json +1 -1
package/bin/skillscokac.js
CHANGED
|
@@ -18,6 +18,45 @@ const VERSION = packageJson.version
|
|
|
18
18
|
|
|
19
19
|
const API_BASE_URL = 'https://skills.cokac.com'
|
|
20
20
|
|
|
21
|
+
// Configuration constants
|
|
22
|
+
const MAX_ZIP_SIZE = 50 * 1024 * 1024 // 50MB
|
|
23
|
+
const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5MB
|
|
24
|
+
const AXIOS_TIMEOUT = 30000 // 30 seconds
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Validate skill name for security
|
|
28
|
+
*/
|
|
29
|
+
function validateSkillName(skillName) {
|
|
30
|
+
if (!skillName || typeof skillName !== 'string') {
|
|
31
|
+
throw new Error('Invalid skill name')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const trimmed = skillName.trim()
|
|
35
|
+
|
|
36
|
+
if (trimmed.length === 0) {
|
|
37
|
+
throw new Error('Skill name cannot be empty')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (trimmed.includes('/') || trimmed.includes('\\')) {
|
|
41
|
+
throw new Error('Skill name cannot contain path separators')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (trimmed.includes('..')) {
|
|
45
|
+
throw new Error('Skill name cannot contain ".."')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (trimmed.startsWith('.')) {
|
|
49
|
+
throw new Error('Skill name cannot start with a dot')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Check for other potentially dangerous characters
|
|
53
|
+
if (/[<>:"|?*\x00-\x1f]/.test(trimmed)) {
|
|
54
|
+
throw new Error('Skill name contains invalid characters')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return trimmed
|
|
58
|
+
}
|
|
59
|
+
|
|
21
60
|
/**
|
|
22
61
|
* Parse frontmatter from markdown content
|
|
23
62
|
*/
|
|
@@ -79,12 +118,16 @@ async function fetchSkill(skillName, options = {}) {
|
|
|
79
118
|
const spinner = silent ? null : ora(`Searching for skill: ${skillName}`).start()
|
|
80
119
|
|
|
81
120
|
try {
|
|
121
|
+
// Validate skill name
|
|
122
|
+
skillName = validateSkillName(skillName)
|
|
123
|
+
|
|
82
124
|
// Step 1: Get marketplace data to find postId
|
|
83
125
|
if (spinner) spinner.text = 'Fetching marketplace data...'
|
|
84
126
|
const marketplaceResponse = await axios.get(`${API_BASE_URL}/api/marketplace`, {
|
|
85
127
|
headers: {
|
|
86
128
|
'User-Agent': `skillscokac-cli/${VERSION}`
|
|
87
|
-
}
|
|
129
|
+
},
|
|
130
|
+
timeout: AXIOS_TIMEOUT
|
|
88
131
|
})
|
|
89
132
|
|
|
90
133
|
const marketplace = marketplaceResponse.data
|
|
@@ -125,15 +168,34 @@ async function fetchSkill(skillName, options = {}) {
|
|
|
125
168
|
responseType: 'arraybuffer',
|
|
126
169
|
headers: {
|
|
127
170
|
'User-Agent': `skillscokac-cli/${VERSION}`
|
|
128
|
-
}
|
|
171
|
+
},
|
|
172
|
+
timeout: AXIOS_TIMEOUT,
|
|
173
|
+
maxContentLength: MAX_ZIP_SIZE
|
|
129
174
|
}
|
|
130
175
|
)
|
|
131
176
|
|
|
177
|
+
// Check ZIP file size
|
|
178
|
+
const zipSize = zipResponse.data.byteLength
|
|
179
|
+
if (zipSize > MAX_ZIP_SIZE) {
|
|
180
|
+
throw new Error(`Skill package too large (${Math.round(zipSize / 1024 / 1024)}MB). Maximum: ${Math.round(MAX_ZIP_SIZE / 1024 / 1024)}MB`)
|
|
181
|
+
}
|
|
182
|
+
|
|
132
183
|
// Step 3: Extract ZIP
|
|
133
184
|
if (spinner) spinner.text = 'Extracting files...'
|
|
134
185
|
const zip = new AdmZip(Buffer.from(zipResponse.data))
|
|
135
186
|
const zipEntries = zip.getEntries()
|
|
136
187
|
|
|
188
|
+
// Check total uncompressed size
|
|
189
|
+
let totalSize = 0
|
|
190
|
+
for (const entry of zipEntries) {
|
|
191
|
+
if (!entry.isDirectory) {
|
|
192
|
+
totalSize += entry.header.size
|
|
193
|
+
if (totalSize > MAX_ZIP_SIZE * 2) {
|
|
194
|
+
throw new Error('Skill package contains too much data (possible zip bomb)')
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
137
199
|
// Find SKILL.md
|
|
138
200
|
const skillMdEntry = zipEntries.find(entry =>
|
|
139
201
|
entry.entryName.endsWith('SKILL.md') && !entry.isDirectory
|
|
@@ -309,6 +371,14 @@ async function installSkill(skill, installType, options = {}) {
|
|
|
309
371
|
* Install skill command handler
|
|
310
372
|
*/
|
|
311
373
|
async function installSkillCommand(skillName) {
|
|
374
|
+
// Validate skill name (fetchSkill also validates, but do it early)
|
|
375
|
+
try {
|
|
376
|
+
skillName = validateSkillName(skillName)
|
|
377
|
+
} catch (error) {
|
|
378
|
+
console.log(chalk.red(`✗ ${error.message}`))
|
|
379
|
+
process.exit(1)
|
|
380
|
+
}
|
|
381
|
+
|
|
312
382
|
// Fetch skill
|
|
313
383
|
const skill = await fetchSkill(skillName)
|
|
314
384
|
|
|
@@ -333,7 +403,8 @@ async function installCollectionCommand(collectionId) {
|
|
|
333
403
|
const response = await axios.get(`${API_BASE_URL}/api/collections/${collectionId}`, {
|
|
334
404
|
headers: {
|
|
335
405
|
'User-Agent': `skillscokac-cli/${VERSION}`
|
|
336
|
-
}
|
|
406
|
+
},
|
|
407
|
+
timeout: AXIOS_TIMEOUT
|
|
337
408
|
})
|
|
338
409
|
|
|
339
410
|
const collection = response.data
|
|
@@ -468,6 +539,14 @@ function getSkillsFromDirectory(skillsDir) {
|
|
|
468
539
|
* Remove skill command handler
|
|
469
540
|
*/
|
|
470
541
|
async function removeSkillCommand(skillName, force = false) {
|
|
542
|
+
// Validate skill name
|
|
543
|
+
try {
|
|
544
|
+
skillName = validateSkillName(skillName)
|
|
545
|
+
} catch (error) {
|
|
546
|
+
console.log(chalk.red(`✗ ${error.message}`))
|
|
547
|
+
process.exit(1)
|
|
548
|
+
}
|
|
549
|
+
|
|
471
550
|
// Check if skill exists in personal and/or project directories
|
|
472
551
|
const personalSkillDir = path.join(os.homedir(), '.claude', 'skills', skillName)
|
|
473
552
|
const projectSkillDir = path.join(process.cwd(), '.claude', 'skills', skillName)
|
|
@@ -679,6 +758,14 @@ async function removeAllSkillsCommand(force = false) {
|
|
|
679
758
|
* Download skill command handler
|
|
680
759
|
*/
|
|
681
760
|
async function downloadSkillCommand(skillName, downloadPath) {
|
|
761
|
+
// Validate skill name (fetchSkill also validates, but do it early)
|
|
762
|
+
try {
|
|
763
|
+
skillName = validateSkillName(skillName)
|
|
764
|
+
} catch (error) {
|
|
765
|
+
console.log(chalk.red(`✗ ${error.message}`))
|
|
766
|
+
process.exit(1)
|
|
767
|
+
}
|
|
768
|
+
|
|
682
769
|
// Use current directory if no path specified
|
|
683
770
|
if (!downloadPath) {
|
|
684
771
|
downloadPath = process.cwd()
|
|
@@ -847,7 +934,7 @@ async function listInstalledSkillsCommand() {
|
|
|
847
934
|
/**
|
|
848
935
|
* Create skill on SkillsCokac API
|
|
849
936
|
*/
|
|
850
|
-
async function createSkill(skillData, apiKey) {
|
|
937
|
+
async function createSkill(skillData, apiKey, silent = false) {
|
|
851
938
|
const payload = {
|
|
852
939
|
type: 'SKILL',
|
|
853
940
|
title: skillData.name,
|
|
@@ -858,31 +945,31 @@ async function createSkill(skillData, apiKey) {
|
|
|
858
945
|
tags: skillData.tags || ['claude-code', 'agent-skill']
|
|
859
946
|
}
|
|
860
947
|
|
|
861
|
-
const spinner = ora('Creating skill on SkillsCokac...').start()
|
|
862
|
-
|
|
863
948
|
try {
|
|
864
949
|
const response = await axios.post(`${API_BASE_URL}/api/posts`, payload, {
|
|
865
950
|
headers: {
|
|
866
951
|
'Authorization': `Bearer ${apiKey}`,
|
|
867
952
|
'Content-Type': 'application/json',
|
|
868
953
|
'User-Agent': `skillscokac-cli/${VERSION}`
|
|
869
|
-
}
|
|
954
|
+
},
|
|
955
|
+
timeout: AXIOS_TIMEOUT
|
|
870
956
|
})
|
|
871
957
|
|
|
872
|
-
|
|
873
|
-
const skill = response.data
|
|
874
|
-
console.log(chalk.dim(' ID: ') + chalk.cyan(skill.id))
|
|
875
|
-
console.log(chalk.dim(' URL: ') + chalk.cyan(`https://skills.cokac.com/s/${skill.id}`))
|
|
876
|
-
console.log()
|
|
877
|
-
|
|
878
|
-
return skill
|
|
958
|
+
return response.data
|
|
879
959
|
} catch (error) {
|
|
880
|
-
|
|
881
|
-
if (error.response) {
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
960
|
+
// Handle 409 Conflict (skill name already exists)
|
|
961
|
+
if (error.response && error.response.status === 409) {
|
|
962
|
+
if (!silent) {
|
|
963
|
+
console.log(chalk.red('✗ Skill name already exists. Please choose a different name.'))
|
|
964
|
+
}
|
|
965
|
+
} else if (!silent) {
|
|
966
|
+
console.log(chalk.red('✗ Failed to create skill'))
|
|
967
|
+
if (error.response) {
|
|
968
|
+
console.error(chalk.red(' Status:'), error.response.status)
|
|
969
|
+
console.error(chalk.red(' Response:'), error.response.data)
|
|
970
|
+
} else {
|
|
971
|
+
console.error(chalk.red(' Error:'), error.message)
|
|
972
|
+
}
|
|
886
973
|
}
|
|
887
974
|
throw error
|
|
888
975
|
}
|
|
@@ -891,9 +978,7 @@ async function createSkill(skillData, apiKey) {
|
|
|
891
978
|
/**
|
|
892
979
|
* Upload skill files to SkillsCokac API
|
|
893
980
|
*/
|
|
894
|
-
async function uploadSkillFiles(skillId, skillDir, apiKey) {
|
|
895
|
-
const spinner = ora('Uploading additional files...').start()
|
|
896
|
-
|
|
981
|
+
async function uploadSkillFiles(skillId, skillDir, apiKey, silent = false) {
|
|
897
982
|
let uploadedCount = 0
|
|
898
983
|
let failedCount = 0
|
|
899
984
|
const files = []
|
|
@@ -932,21 +1017,27 @@ async function uploadSkillFiles(skillId, skillDir, apiKey) {
|
|
|
932
1017
|
findFiles(skillDir, skillDir)
|
|
933
1018
|
|
|
934
1019
|
if (files.length === 0) {
|
|
935
|
-
|
|
936
|
-
return
|
|
1020
|
+
return { uploadedCount: 0, failedCount: 0 }
|
|
937
1021
|
}
|
|
938
1022
|
|
|
939
|
-
spinner.text = `Found ${files.length} file${files.length !== 1 ? 's' : ''} to upload...`
|
|
940
|
-
|
|
941
1023
|
for (const file of files) {
|
|
942
1024
|
try {
|
|
1025
|
+
// Check file size before reading
|
|
1026
|
+
const stats = fs.statSync(file.fullPath)
|
|
1027
|
+
if (stats.size > MAX_FILE_SIZE) {
|
|
1028
|
+
if (!silent) {
|
|
1029
|
+
console.warn(chalk.yellow(`⚠ Skipping large file (${Math.round(stats.size / 1024 / 1024)}MB): ${file.relativePath}`))
|
|
1030
|
+
}
|
|
1031
|
+
failedCount++
|
|
1032
|
+
continue
|
|
1033
|
+
}
|
|
1034
|
+
|
|
943
1035
|
// Try to read as text
|
|
944
1036
|
let content
|
|
945
1037
|
try {
|
|
946
1038
|
content = fs.readFileSync(file.fullPath, 'utf8')
|
|
947
1039
|
} catch (err) {
|
|
948
1040
|
// Skip binary files
|
|
949
|
-
spinner.warn(chalk.yellow(` Skipping binary file: ${file.relativePath}`))
|
|
950
1041
|
continue
|
|
951
1042
|
}
|
|
952
1043
|
|
|
@@ -955,8 +1046,6 @@ async function uploadSkillFiles(skillId, skillDir, apiKey) {
|
|
|
955
1046
|
content: content
|
|
956
1047
|
}
|
|
957
1048
|
|
|
958
|
-
spinner.text = `Uploading: ${file.relativePath}`
|
|
959
|
-
|
|
960
1049
|
const response = await axios.post(
|
|
961
1050
|
`${API_BASE_URL}/api/posts/${skillId}/files`,
|
|
962
1051
|
filePayload,
|
|
@@ -965,7 +1054,8 @@ async function uploadSkillFiles(skillId, skillDir, apiKey) {
|
|
|
965
1054
|
'Authorization': `Bearer ${apiKey}`,
|
|
966
1055
|
'Content-Type': 'application/json',
|
|
967
1056
|
'User-Agent': `skillscokac-cli/${VERSION}`
|
|
968
|
-
}
|
|
1057
|
+
},
|
|
1058
|
+
timeout: AXIOS_TIMEOUT
|
|
969
1059
|
}
|
|
970
1060
|
)
|
|
971
1061
|
|
|
@@ -976,22 +1066,14 @@ async function uploadSkillFiles(skillId, skillDir, apiKey) {
|
|
|
976
1066
|
}
|
|
977
1067
|
} catch (error) {
|
|
978
1068
|
failedCount++
|
|
979
|
-
console.error(chalk.red(`\n ✗ Failed to upload: ${file.relativePath}`))
|
|
980
|
-
if (error.response) {
|
|
981
|
-
console.error(chalk.red(' Status:'), error.response.status)
|
|
982
|
-
}
|
|
983
1069
|
}
|
|
984
1070
|
}
|
|
985
1071
|
|
|
986
|
-
|
|
987
|
-
spinner.succeed(chalk.green(`Uploaded ${uploadedCount} file${uploadedCount !== 1 ? 's' : ''}`))
|
|
988
|
-
} else {
|
|
989
|
-
spinner.warn(chalk.yellow(`Uploaded ${uploadedCount}, Failed ${failedCount}`))
|
|
990
|
-
}
|
|
991
|
-
console.log()
|
|
992
|
-
|
|
1072
|
+
return { uploadedCount, failedCount }
|
|
993
1073
|
} catch (error) {
|
|
994
|
-
|
|
1074
|
+
if (!silent) {
|
|
1075
|
+
console.error(chalk.red('Failed to upload files'))
|
|
1076
|
+
}
|
|
995
1077
|
throw error
|
|
996
1078
|
}
|
|
997
1079
|
}
|
|
@@ -1027,21 +1109,10 @@ async function uploadSkillCommand(skillDir, apiKey) {
|
|
|
1027
1109
|
process.exit(1)
|
|
1028
1110
|
}
|
|
1029
1111
|
|
|
1030
|
-
|
|
1031
|
-
chalk.bold.cyan('SkillsCokac Skill Uploader'),
|
|
1032
|
-
{
|
|
1033
|
-
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
1034
|
-
borderStyle: 'round',
|
|
1035
|
-
borderColor: 'cyan'
|
|
1036
|
-
}
|
|
1037
|
-
))
|
|
1038
|
-
console.log()
|
|
1039
|
-
console.log(chalk.dim(' Directory: ') + chalk.white(resolvedSkillDir))
|
|
1040
|
-
console.log()
|
|
1041
|
-
|
|
1112
|
+
let spinner
|
|
1042
1113
|
try {
|
|
1043
1114
|
// Step 1: Parse SKILL.md
|
|
1044
|
-
|
|
1115
|
+
spinner = ora('Uploading skill...').start()
|
|
1045
1116
|
const skillMdContent = fs.readFileSync(skillMdPath, 'utf8')
|
|
1046
1117
|
const { metadata } = parseFrontmatter(skillMdContent)
|
|
1047
1118
|
|
|
@@ -1063,31 +1134,27 @@ async function uploadSkillCommand(skillDir, apiKey) {
|
|
|
1063
1134
|
tags: ['claude-code', 'agent-skill']
|
|
1064
1135
|
}
|
|
1065
1136
|
|
|
1066
|
-
spinner.succeed(chalk.green('SKILL.md parsed'))
|
|
1067
|
-
console.log(chalk.dim(' Name: ') + chalk.white(skillData.name))
|
|
1068
|
-
console.log(chalk.dim(' Description: ') + chalk.white(skillData.description))
|
|
1069
|
-
console.log()
|
|
1070
|
-
|
|
1071
1137
|
// Step 2: Create skill
|
|
1072
|
-
|
|
1138
|
+
spinner.text = 'Creating skill...'
|
|
1139
|
+
const skill = await createSkill(skillData, apiKey, true)
|
|
1073
1140
|
|
|
1074
1141
|
// Step 3: Upload additional files
|
|
1075
|
-
|
|
1142
|
+
spinner.text = 'Uploading files...'
|
|
1143
|
+
const { uploadedCount, failedCount } = await uploadSkillFiles(skill.id, resolvedSkillDir, apiKey, true)
|
|
1076
1144
|
|
|
1077
1145
|
// Success summary
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
{
|
|
1082
|
-
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
1083
|
-
borderStyle: 'round',
|
|
1084
|
-
borderColor: 'green'
|
|
1085
|
-
}
|
|
1086
|
-
))
|
|
1087
|
-
console.log()
|
|
1146
|
+
const fileInfo = uploadedCount > 0 ? ` (${uploadedCount} file${uploadedCount !== 1 ? 's' : ''})` : ''
|
|
1147
|
+
spinner.succeed(chalk.green(`Uploaded: ${skillData.name}${fileInfo}`))
|
|
1148
|
+
console.log(chalk.cyan(`https://skills.cokac.com/p/${skill.id}`))
|
|
1088
1149
|
|
|
1089
1150
|
} catch (error) {
|
|
1090
|
-
|
|
1151
|
+
if (spinner) spinner.stop()
|
|
1152
|
+
// Handle 409 Conflict (skill name already exists)
|
|
1153
|
+
if (error.response && error.response.status === 409) {
|
|
1154
|
+
console.log(chalk.red('✗ Skill name already exists. Please choose a different name.'))
|
|
1155
|
+
} else {
|
|
1156
|
+
console.error(chalk.red('✗ Upload failed:'), error.message)
|
|
1157
|
+
}
|
|
1091
1158
|
process.exit(1)
|
|
1092
1159
|
}
|
|
1093
1160
|
}
|