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.
Files changed (2) hide show
  1. package/bin/skillscokac.js +141 -74
  2. package/package.json +1 -1
@@ -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
- spinner.succeed(chalk.green('Skill created successfully!'))
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
- spinner.fail(chalk.red('Failed to create skill'))
881
- if (error.response) {
882
- console.error(chalk.red(' Status:'), error.response.status)
883
- console.error(chalk.red(' Response:'), error.response.data)
884
- } else {
885
- console.error(chalk.red(' Error:'), error.message)
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
- spinner.succeed(chalk.green('No additional files to upload'))
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
- if (failedCount === 0) {
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
- spinner.fail(chalk.red('Failed to upload files'))
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
- console.log(boxen(
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
- const spinner = ora('Parsing SKILL.md...').start()
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
- const skill = await createSkill(skillData, apiKey)
1138
+ spinner.text = 'Creating skill...'
1139
+ const skill = await createSkill(skillData, apiKey, true)
1073
1140
 
1074
1141
  // Step 3: Upload additional files
1075
- await uploadSkillFiles(skill.id, resolvedSkillDir, apiKey)
1142
+ spinner.text = 'Uploading files...'
1143
+ const { uploadedCount, failedCount } = await uploadSkillFiles(skill.id, resolvedSkillDir, apiKey, true)
1076
1144
 
1077
1145
  // Success summary
1078
- console.log(boxen(
1079
- chalk.bold.green('✓ Upload complete!\n\n') +
1080
- chalk.dim('Skill URL: ') + chalk.cyan(`https://skills.cokac.com/s/${skill.id}`),
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
- console.error(chalk.red('\n✗ Upload failed:'), error.message)
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillscokac",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "CLI tool to install and manage Claude Code skills from skills.cokac.com",
5
5
  "main": "bin/skillscokac.js",
6
6
  "bin": {