skillscokac 1.0.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/README.md CHANGED
@@ -30,6 +30,9 @@ The CLI will:
30
30
  |---------|-------------|
31
31
  | `-i, --install-skill <skillName>` | Install a single skill |
32
32
  | `-c, --install-collection <collectionId>` | Install all skills from a collection |
33
+ | `-d, --download <skillName> [path]` | Download a skill to a directory (defaults to current directory) |
34
+ | `-u, --upload <skillDir>` | Upload a skill to skills.cokac.com (requires `--apikey`) |
35
+ | `--apikey <key>` | API key for uploading skills |
33
36
  | `-l, --list-installed-skills` | List all installed skills |
34
37
  | `-r, --remove-skill <skillName>` | Remove an installed skill (with confirmation) |
35
38
  | `-f, --remove-skill-force <skillName>` | Remove a skill without confirmation |
@@ -48,6 +51,24 @@ npx skillscokac -i my-awesome-skill
48
51
  npx skillscokac -c collection-id-here
49
52
  ```
50
53
 
54
+ **Download a skill to a specific directory:**
55
+ ```bash
56
+ # Download to a specific path
57
+ npx skillscokac -d my-awesome-skill ./downloads
58
+
59
+ # Download to current directory (path is optional)
60
+ npx skillscokac -d my-awesome-skill
61
+ ```
62
+
63
+ This will download the skill without installing it to Claude Code's skill directories.
64
+
65
+ **Upload a skill to skills.cokac.com:**
66
+ ```bash
67
+ npx skillscokac --upload ./my-skill --apikey ck_live_xxxxx
68
+ ```
69
+
70
+ This will upload your skill to the marketplace. Requires an API key from skills.cokac.com.
71
+
51
72
  **List installed skills:**
52
73
  ```bash
53
74
  npx skillscokac -l
@@ -77,6 +98,97 @@ When installing, you'll be prompted to choose between:
77
98
  - **Scope**: Available only in the current project
78
99
  - **Use case**: Project-specific skills or testing before making them global
79
100
 
101
+ ## Download to Custom Location
102
+
103
+ If you want to download a skill without installing it to Claude Code's skill directories, use the download command:
104
+
105
+ ```bash
106
+ npx skillscokac -d <skill-name> [path]
107
+ ```
108
+
109
+ The `path` parameter is optional and defaults to the current directory.
110
+
111
+ **Examples:**
112
+ ```bash
113
+ # Download to current directory
114
+ npx skillscokac -d my-skill
115
+
116
+ # Download to specific path
117
+ npx skillscokac -d my-skill ./my-downloads
118
+
119
+ # Download to absolute path
120
+ npx skillscokac -d my-skill /path/to/directory
121
+ ```
122
+
123
+ This will:
124
+ 1. Fetch the skill from skills.cokac.com
125
+ 2. Display skill information
126
+ 3. Download all skill files to `<path>/my-skill/`
127
+ 4. **Not** install it to Claude Code's skill directories
128
+
129
+ **Use cases:**
130
+ - Inspecting skill contents before installation
131
+ - Backing up skills locally
132
+ - Sharing skill files with team members
133
+ - Custom deployment workflows
134
+
135
+ ## Upload Skills to Marketplace
136
+
137
+ You can upload your own skills to skills.cokac.com using the upload command:
138
+
139
+ ```bash
140
+ npx skillscokac --upload <skillDir> --apikey <your-api-key>
141
+ ```
142
+
143
+ ### Requirements
144
+
145
+ 1. **API Key**: Get your API key from [skills.cokac.com](https://skills.cokac.com) (format: `ck_live_xxxxx`)
146
+ 2. **SKILL.md**: Your skill directory must contain a `SKILL.md` file with frontmatter:
147
+
148
+ ```markdown
149
+ ---
150
+ name: my-skill
151
+ description: A brief description of what this skill does
152
+ version: 1.0.0
153
+ ---
154
+
155
+ # My Skill
156
+
157
+ Your skill instructions here...
158
+ ```
159
+
160
+ ### Upload Process
161
+
162
+ The CLI will:
163
+ 1. Parse the `SKILL.md` file and extract metadata (name, description)
164
+ 2. Create a new skill post on skills.cokac.com
165
+ 3. Upload all additional files in the directory (excluding hidden files and common ignore patterns)
166
+ 4. Return the skill URL
167
+
168
+ ### Example
169
+
170
+ ```bash
171
+ # Upload a skill from current directory
172
+ npx skillscokac --upload . --apikey ck_live_xxxxx
173
+
174
+ # Upload a skill from specific directory
175
+ npx skillscokac --upload ./skills/my-awesome-skill --apikey ck_live_xxxxx
176
+ ```
177
+
178
+ ### What Gets Uploaded
179
+
180
+ - **SKILL.md**: Main skill file (required, used as primary content)
181
+ - **Additional files**: Any other files in the directory (e.g., scripts, configs, examples)
182
+ - **Excluded**: Hidden files (`.git`, `.env`), `node_modules`, `__pycache__`
183
+
184
+ ### API Key
185
+
186
+ To get an API key:
187
+ 1. Visit [skills.cokac.com](https://skills.cokac.com)
188
+ 2. Sign in to your account
189
+ 3. Go to Settings → API Keys
190
+ 4. Generate a new API key
191
+
80
192
  ## Using Installed Skills
81
193
 
82
194
  After installation, use the skill in Claude Code with:
@@ -166,11 +278,13 @@ When you install a skill, the CLI downloads and extracts:
166
278
 
167
279
  This CLI communicates with the following endpoints:
168
280
 
169
- | Endpoint | Purpose |
170
- |----------|---------|
171
- | `GET /api/marketplace` | Fetch marketplace data to find skills |
172
- | `GET /api/posts/{postId}/export-skill-zip` | Download skill ZIP package |
173
- | `GET /api/collections/{collectionId}` | Fetch collection metadata and skills |
281
+ | Endpoint | Method | Purpose |
282
+ |----------|--------|---------|
283
+ | `/api/marketplace` | GET | Fetch marketplace data to find skills |
284
+ | `/api/posts/{postId}/export-skill-zip` | GET | Download skill ZIP package |
285
+ | `/api/collections/{collectionId}` | GET | Fetch collection metadata and skills |
286
+ | `/api/posts` | POST | Create a new skill (requires API key) |
287
+ | `/api/posts/{postId}/files` | POST | Upload additional files to a skill (requires API key) |
174
288
 
175
289
  **Base URL**: `https://skills.cokac.com`
176
290
 
@@ -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)
@@ -675,6 +754,91 @@ async function removeAllSkillsCommand(force = false) {
675
754
  }
676
755
  }
677
756
 
757
+ /**
758
+ * Download skill command handler
759
+ */
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
+
769
+ // Use current directory if no path specified
770
+ if (!downloadPath) {
771
+ downloadPath = process.cwd()
772
+ }
773
+
774
+ // Fetch skill
775
+ const skill = await fetchSkill(skillName)
776
+
777
+ // Display skill info
778
+ displaySkillInfo(skill)
779
+ console.log()
780
+
781
+ // Resolve download path
782
+ const resolvedPath = path.resolve(downloadPath)
783
+ const targetDir = path.join(resolvedPath, skill.skillName)
784
+
785
+ const spinner = ora('Downloading skill...').start()
786
+
787
+ try {
788
+ // Create target directory
789
+ if (fs.existsSync(targetDir)) {
790
+ spinner.text = 'Removing existing directory...'
791
+ fs.rmSync(targetDir, { recursive: true, force: true })
792
+ }
793
+
794
+ spinner.text = 'Creating directory...'
795
+ fs.mkdirSync(targetDir, { recursive: true })
796
+
797
+ // Write SKILL.md
798
+ spinner.text = 'Writing files...'
799
+ const skillMdPath = path.join(targetDir, 'SKILL.md')
800
+ fs.writeFileSync(skillMdPath, skill.skillMd || '', 'utf8')
801
+
802
+ // Write additional files
803
+ if (skill.files && skill.files.length > 0) {
804
+ for (const file of skill.files) {
805
+ // Security: Prevent path traversal attacks
806
+ const normalizedPath = path.normalize(file.path).replace(/^(\.\.(\/|\\|$))+/, '')
807
+ const filePath = path.join(targetDir, normalizedPath)
808
+
809
+ // Verify that the final path is still within targetDir
810
+ const resolvedFilePath = path.resolve(filePath)
811
+ const resolvedTargetDir = path.resolve(targetDir)
812
+
813
+ if (!resolvedFilePath.startsWith(resolvedTargetDir + path.sep) && resolvedFilePath !== resolvedTargetDir) {
814
+ console.warn(chalk.yellow(`⚠ Skipping potentially unsafe file path: ${file.path}`))
815
+ continue
816
+ }
817
+
818
+ const fileDir = path.dirname(filePath)
819
+
820
+ // Create subdirectories if needed
821
+ if (!fs.existsSync(fileDir)) {
822
+ fs.mkdirSync(fileDir, { recursive: true })
823
+ }
824
+
825
+ fs.writeFileSync(filePath, file.content || '', 'utf8')
826
+ }
827
+ }
828
+
829
+ spinner.succeed(chalk.green('Downloaded successfully!'))
830
+ console.log()
831
+ console.log(chalk.dim(' Location: ') + chalk.cyan(targetDir))
832
+ console.log(chalk.dim(' Files: ') + chalk.white(`${1 + (skill.files ? skill.files.length : 0)} file${skill.files && skill.files.length !== 0 ? 's' : ''}`))
833
+ console.log()
834
+
835
+ } catch (error) {
836
+ spinner.fail(chalk.red('Failed to download skill'))
837
+ console.error(chalk.red('\nError:'), error.message)
838
+ process.exit(1)
839
+ }
840
+ }
841
+
678
842
  /**
679
843
  * List installed skills command handler
680
844
  */
@@ -767,6 +931,234 @@ async function listInstalledSkillsCommand() {
767
931
  }
768
932
  }
769
933
 
934
+ /**
935
+ * Create skill on SkillsCokac API
936
+ */
937
+ async function createSkill(skillData, apiKey, silent = false) {
938
+ const payload = {
939
+ type: 'SKILL',
940
+ title: skillData.name,
941
+ description: skillData.description,
942
+ content: skillData.content,
943
+ skillMd: skillData.content,
944
+ visibility: skillData.visibility || 'PUBLIC',
945
+ tags: skillData.tags || ['claude-code', 'agent-skill']
946
+ }
947
+
948
+ try {
949
+ const response = await axios.post(`${API_BASE_URL}/api/posts`, payload, {
950
+ headers: {
951
+ 'Authorization': `Bearer ${apiKey}`,
952
+ 'Content-Type': 'application/json',
953
+ 'User-Agent': `skillscokac-cli/${VERSION}`
954
+ },
955
+ timeout: AXIOS_TIMEOUT
956
+ })
957
+
958
+ return response.data
959
+ } catch (error) {
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
+ }
973
+ }
974
+ throw error
975
+ }
976
+ }
977
+
978
+ /**
979
+ * Upload skill files to SkillsCokac API
980
+ */
981
+ async function uploadSkillFiles(skillId, skillDir, apiKey, silent = false) {
982
+ let uploadedCount = 0
983
+ let failedCount = 0
984
+ const files = []
985
+
986
+ // Recursively find all files in the directory
987
+ function findFiles(dir, baseDir) {
988
+ const entries = fs.readdirSync(dir, { withFileTypes: true })
989
+
990
+ for (const entry of entries) {
991
+ const fullPath = path.join(dir, entry.name)
992
+
993
+ if (entry.isDirectory()) {
994
+ // Skip hidden directories and common ignore patterns
995
+ if (entry.name.startsWith('.') || entry.name === '__pycache__' || entry.name === 'node_modules') {
996
+ continue
997
+ }
998
+ findFiles(fullPath, baseDir)
999
+ } else if (entry.isFile()) {
1000
+ // Skip SKILL.md at root level (already uploaded as main content)
1001
+ const relativePath = path.relative(baseDir, fullPath)
1002
+ if (relativePath === 'SKILL.md') {
1003
+ continue
1004
+ }
1005
+
1006
+ // Skip hidden files
1007
+ if (entry.name.startsWith('.')) {
1008
+ continue
1009
+ }
1010
+
1011
+ files.push({ fullPath, relativePath })
1012
+ }
1013
+ }
1014
+ }
1015
+
1016
+ try {
1017
+ findFiles(skillDir, skillDir)
1018
+
1019
+ if (files.length === 0) {
1020
+ return { uploadedCount: 0, failedCount: 0 }
1021
+ }
1022
+
1023
+ for (const file of files) {
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
+
1035
+ // Try to read as text
1036
+ let content
1037
+ try {
1038
+ content = fs.readFileSync(file.fullPath, 'utf8')
1039
+ } catch (err) {
1040
+ // Skip binary files
1041
+ continue
1042
+ }
1043
+
1044
+ const filePayload = {
1045
+ path: file.relativePath.replace(/\\/g, '/'), // Normalize Windows paths
1046
+ content: content
1047
+ }
1048
+
1049
+ const response = await axios.post(
1050
+ `${API_BASE_URL}/api/posts/${skillId}/files`,
1051
+ filePayload,
1052
+ {
1053
+ headers: {
1054
+ 'Authorization': `Bearer ${apiKey}`,
1055
+ 'Content-Type': 'application/json',
1056
+ 'User-Agent': `skillscokac-cli/${VERSION}`
1057
+ },
1058
+ timeout: AXIOS_TIMEOUT
1059
+ }
1060
+ )
1061
+
1062
+ if (response.status === 201) {
1063
+ uploadedCount++
1064
+ } else {
1065
+ failedCount++
1066
+ }
1067
+ } catch (error) {
1068
+ failedCount++
1069
+ }
1070
+ }
1071
+
1072
+ return { uploadedCount, failedCount }
1073
+ } catch (error) {
1074
+ if (!silent) {
1075
+ console.error(chalk.red('Failed to upload files'))
1076
+ }
1077
+ throw error
1078
+ }
1079
+ }
1080
+
1081
+ /**
1082
+ * Upload skill command handler
1083
+ */
1084
+ async function uploadSkillCommand(skillDir, apiKey) {
1085
+ // Validate API key
1086
+ if (!apiKey) {
1087
+ console.log(chalk.red('✗ API key is required'))
1088
+ console.log(chalk.dim('Usage: npx skillscokac --upload <skillDir> --apikey <key>'))
1089
+ console.log()
1090
+ process.exit(1)
1091
+ }
1092
+
1093
+ // Resolve skill directory
1094
+ const resolvedSkillDir = path.resolve(skillDir)
1095
+ const skillMdPath = path.join(resolvedSkillDir, 'SKILL.md')
1096
+
1097
+ // Validate directory exists
1098
+ if (!fs.existsSync(resolvedSkillDir)) {
1099
+ console.log(chalk.red(`✗ Directory not found: ${resolvedSkillDir}`))
1100
+ console.log()
1101
+ process.exit(1)
1102
+ }
1103
+
1104
+ // Validate SKILL.md exists
1105
+ if (!fs.existsSync(skillMdPath)) {
1106
+ console.log(chalk.red(`✗ SKILL.md not found in: ${resolvedSkillDir}`))
1107
+ console.log(chalk.dim(' The skill directory must contain a SKILL.md file'))
1108
+ console.log()
1109
+ process.exit(1)
1110
+ }
1111
+
1112
+ let spinner
1113
+ try {
1114
+ // Step 1: Parse SKILL.md
1115
+ spinner = ora('Uploading skill...').start()
1116
+ const skillMdContent = fs.readFileSync(skillMdPath, 'utf8')
1117
+ const { metadata } = parseFrontmatter(skillMdContent)
1118
+
1119
+ if (!metadata.name) {
1120
+ spinner.fail(chalk.red('SKILL.md must have a "name" in frontmatter'))
1121
+ process.exit(1)
1122
+ }
1123
+
1124
+ if (!metadata.description) {
1125
+ spinner.fail(chalk.red('SKILL.md must have a "description" in frontmatter'))
1126
+ process.exit(1)
1127
+ }
1128
+
1129
+ const skillData = {
1130
+ name: metadata.name,
1131
+ description: metadata.description,
1132
+ content: skillMdContent,
1133
+ visibility: 'PUBLIC',
1134
+ tags: ['claude-code', 'agent-skill']
1135
+ }
1136
+
1137
+ // Step 2: Create skill
1138
+ spinner.text = 'Creating skill...'
1139
+ const skill = await createSkill(skillData, apiKey, true)
1140
+
1141
+ // Step 3: Upload additional files
1142
+ spinner.text = 'Uploading files...'
1143
+ const { uploadedCount, failedCount } = await uploadSkillFiles(skill.id, resolvedSkillDir, apiKey, true)
1144
+
1145
+ // Success summary
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}`))
1149
+
1150
+ } catch (error) {
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
+ }
1158
+ process.exit(1)
1159
+ }
1160
+ }
1161
+
770
1162
  /**
771
1163
  * Setup CLI with Commander
772
1164
  */
@@ -780,6 +1172,9 @@ program
780
1172
  program
781
1173
  .option('-i, --install-skill <skillName>', 'Install a skill by name')
782
1174
  .option('-c, --install-collection <collectionId>', 'Install all skills from a collection')
1175
+ .option('-d, --download <args...>', 'Download a skill to a directory (usage: --download <skillName> [path], defaults to current directory)')
1176
+ .option('-u, --upload <skillDir>', 'Upload a skill from a directory (requires --apikey)')
1177
+ .option('--apikey <key>', 'API key for uploading skills')
783
1178
  .option('-r, --remove-skill <skillName>', 'Remove an installed skill')
784
1179
  .option('-f, --remove-skill-force <skillName>', 'Remove skill from all locations without confirmation')
785
1180
  .option('-a, --remove-all-skills', 'Remove all installed skills')
@@ -796,6 +1191,18 @@ const options = program.opts()
796
1191
  await installSkillCommand(options.installSkill)
797
1192
  } else if (options.installCollection) {
798
1193
  await installCollectionCommand(options.installCollection)
1194
+ } else if (options.download) {
1195
+ // Download expects one or two arguments: skillName and optional path
1196
+ if (options.download.length < 1 || options.download.length > 2) {
1197
+ console.log(chalk.red('✗ Invalid arguments for --download'))
1198
+ console.log(chalk.dim('Usage: npx skillscokac --download <skillName> [path]'))
1199
+ console.log()
1200
+ process.exit(1)
1201
+ }
1202
+ const [skillName, downloadPath] = options.download
1203
+ await downloadSkillCommand(skillName, downloadPath)
1204
+ } else if (options.upload) {
1205
+ await uploadSkillCommand(options.upload, options.apikey)
799
1206
  } else if (options.removeAllSkillsForce) {
800
1207
  await removeAllSkillsCommand(true)
801
1208
  } else if (options.removeAllSkills) {
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "skillscokac",
3
- "version": "1.0.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": {
7
- "skillscokac": "./bin/skillscokac.js"
7
+ "skillscokac": "bin/skillscokac.js"
8
8
  },
9
9
  "scripts": {
10
10
  "test": "echo \"Error: no test specified\" && exit 1",
@@ -30,7 +30,7 @@
30
30
  },
31
31
  "repository": {
32
32
  "type": "git",
33
- "url": "https://github.com/kstost/skillscokac.git"
33
+ "url": "git+https://github.com/kstost/skillscokac.git"
34
34
  },
35
35
  "bugs": {
36
36
  "url": "https://github.com/kstost/skillscokac/issues"