skillscokac 1.1.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -17
- package/bin/skillscokac.js +370 -72
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,8 +31,9 @@ The CLI will:
|
|
|
31
31
|
| `-i, --install-skill <skillName>` | Install a single skill |
|
|
32
32
|
| `-c, --install-collection <collectionId>` | Install all skills from a collection |
|
|
33
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
|
-
|
|
|
34
|
+
| `-u, --upload <skillDir>` | Upload a new skill to skills.cokac.com (requires `--apikey`) |
|
|
35
|
+
| `-m, --uploadmodify <skillDir>` | Upload or update a skill (creates if new, updates if exists, requires `--apikey`) |
|
|
36
|
+
| `--apikey <key>` | API key for uploading/updating skills |
|
|
36
37
|
| `-l, --list-installed-skills` | List all installed skills |
|
|
37
38
|
| `-r, --remove-skill <skillName>` | Remove an installed skill (with confirmation) |
|
|
38
39
|
| `-f, --remove-skill-force <skillName>` | Remove a skill without confirmation |
|
|
@@ -69,6 +70,15 @@ npx skillscokac --upload ./my-skill --apikey ck_live_xxxxx
|
|
|
69
70
|
|
|
70
71
|
This will upload your skill to the marketplace. Requires an API key from skills.cokac.com.
|
|
71
72
|
|
|
73
|
+
**Upload or update a skill:**
|
|
74
|
+
```bash
|
|
75
|
+
npx skillscokac --uploadmodify ./my-skill --apikey ck_live_xxxxx
|
|
76
|
+
# or use short option
|
|
77
|
+
npx skillscokac -m ./my-skill --apikey ck_live_xxxxx
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
This will create a new skill if it doesn't exist, or update it if it already exists. Perfect for maintaining and iterating on your skills.
|
|
81
|
+
|
|
72
82
|
**List installed skills:**
|
|
73
83
|
```bash
|
|
74
84
|
npx skillscokac -l
|
|
@@ -137,7 +147,11 @@ This will:
|
|
|
137
147
|
You can upload your own skills to skills.cokac.com using the upload command:
|
|
138
148
|
|
|
139
149
|
```bash
|
|
150
|
+
# Upload a new skill (fails if skill name already exists)
|
|
140
151
|
npx skillscokac --upload <skillDir> --apikey <your-api-key>
|
|
152
|
+
|
|
153
|
+
# Upload or update a skill (creates new or updates existing)
|
|
154
|
+
npx skillscokac --uploadmodify <skillDir> --apikey <your-api-key>
|
|
141
155
|
```
|
|
142
156
|
|
|
143
157
|
### Requirements
|
|
@@ -165,8 +179,9 @@ The CLI will:
|
|
|
165
179
|
3. Upload all additional files in the directory (excluding hidden files and common ignore patterns)
|
|
166
180
|
4. Return the skill URL
|
|
167
181
|
|
|
168
|
-
###
|
|
182
|
+
### Examples
|
|
169
183
|
|
|
184
|
+
**Upload a new skill:**
|
|
170
185
|
```bash
|
|
171
186
|
# Upload a skill from current directory
|
|
172
187
|
npx skillscokac --upload . --apikey ck_live_xxxxx
|
|
@@ -175,6 +190,36 @@ npx skillscokac --upload . --apikey ck_live_xxxxx
|
|
|
175
190
|
npx skillscokac --upload ./skills/my-awesome-skill --apikey ck_live_xxxxx
|
|
176
191
|
```
|
|
177
192
|
|
|
193
|
+
**Upload or update a skill:**
|
|
194
|
+
```bash
|
|
195
|
+
# Create new skill or update if it exists
|
|
196
|
+
npx skillscokac --uploadmodify ./my-skill --apikey ck_live_xxxxx
|
|
197
|
+
|
|
198
|
+
# Short option
|
|
199
|
+
npx skillscokac -m ./my-skill --apikey ck_live_xxxxx
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Difference between --upload and --uploadmodify
|
|
203
|
+
|
|
204
|
+
| Feature | `--upload` | `--uploadmodify` |
|
|
205
|
+
|---------|------------|------------------|
|
|
206
|
+
| **Behavior** | Creates a new skill only | Creates new OR updates existing skill |
|
|
207
|
+
| **If skill exists** | ❌ Fails with error (409 Conflict) | ✅ Updates the existing skill |
|
|
208
|
+
| **If skill doesn't exist** | ✅ Creates new skill | ✅ Creates new skill |
|
|
209
|
+
| **Use case** | First-time upload | Maintaining and iterating on skills |
|
|
210
|
+
| **Update process** | N/A | Replaces all files atomically |
|
|
211
|
+
|
|
212
|
+
**When to use `--upload`:**
|
|
213
|
+
- First time publishing a skill
|
|
214
|
+
- When you want to ensure you're creating a brand new skill
|
|
215
|
+
- When duplicate skill names should be prevented
|
|
216
|
+
|
|
217
|
+
**When to use `--uploadmodify`:**
|
|
218
|
+
- Updating an existing skill with improvements
|
|
219
|
+
- Continuous development workflow
|
|
220
|
+
- When you want "create or update" behavior
|
|
221
|
+
- Maintaining published skills over time
|
|
222
|
+
|
|
178
223
|
### What Gets Uploaded
|
|
179
224
|
|
|
180
225
|
- **SKILL.md**: Main skill file (required, used as primary content)
|
|
@@ -274,20 +319,6 @@ When you install a skill, the CLI downloads and extracts:
|
|
|
274
319
|
- **Node.js**: 14.0.0 or higher
|
|
275
320
|
- **Claude Code**: Installed and configured
|
|
276
321
|
|
|
277
|
-
## API Endpoints
|
|
278
|
-
|
|
279
|
-
This CLI communicates with the following endpoints:
|
|
280
|
-
|
|
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) |
|
|
288
|
-
|
|
289
|
-
**Base URL**: `https://skills.cokac.com`
|
|
290
|
-
|
|
291
322
|
## Development
|
|
292
323
|
|
|
293
324
|
### Local Testing
|
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,255 @@ 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
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
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
|
+
|
|
1162
|
+
/**
|
|
1163
|
+
* Find skill by name
|
|
1164
|
+
*/
|
|
1165
|
+
async function findSkillByName(skillName, apiKey) {
|
|
1166
|
+
try {
|
|
1167
|
+
const response = await axios.get(`${API_BASE_URL}/api/skills/${skillName}`, {
|
|
1168
|
+
headers: {
|
|
1169
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
1170
|
+
'User-Agent': `skillscokac-cli/${VERSION}`
|
|
1171
|
+
},
|
|
1172
|
+
timeout: AXIOS_TIMEOUT
|
|
1173
|
+
})
|
|
1174
|
+
return response.data
|
|
1175
|
+
} catch (error) {
|
|
1176
|
+
if (error.response && error.response.status === 404) {
|
|
1177
|
+
return null
|
|
1178
|
+
}
|
|
1179
|
+
throw error
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
/**
|
|
1184
|
+
* Update skill using batch API
|
|
1185
|
+
*/
|
|
1186
|
+
async function updateSkillWithBatch(postId, skillMdContent, skillDir, apiKey, silent = false) {
|
|
1187
|
+
// Collect all files to upload
|
|
1188
|
+
const files = []
|
|
1189
|
+
|
|
1190
|
+
function findFiles(dir, baseDir) {
|
|
1191
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
1192
|
+
|
|
1193
|
+
for (const entry of entries) {
|
|
1194
|
+
const fullPath = path.join(dir, entry.name)
|
|
1195
|
+
|
|
1196
|
+
if (entry.isDirectory()) {
|
|
1197
|
+
if (entry.name.startsWith('.') || entry.name === '__pycache__' || entry.name === 'node_modules') {
|
|
1198
|
+
continue
|
|
1199
|
+
}
|
|
1200
|
+
findFiles(fullPath, baseDir)
|
|
1201
|
+
} else if (entry.isFile()) {
|
|
1202
|
+
const relativePath = path.relative(baseDir, fullPath)
|
|
1203
|
+
if (relativePath === 'SKILL.md') {
|
|
1204
|
+
continue
|
|
1205
|
+
}
|
|
1206
|
+
if (entry.name.startsWith('.')) {
|
|
1207
|
+
continue
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// Check file size
|
|
1211
|
+
const stats = fs.statSync(fullPath)
|
|
1212
|
+
if (stats.size > MAX_FILE_SIZE) {
|
|
1213
|
+
if (!silent) {
|
|
1214
|
+
console.warn(chalk.yellow(`⚠ Skipping large file (${Math.round(stats.size / 1024 / 1024)}MB): ${relativePath}`))
|
|
1215
|
+
}
|
|
1216
|
+
continue
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// Try to read as text
|
|
1220
|
+
let content
|
|
1221
|
+
try {
|
|
1222
|
+
content = fs.readFileSync(fullPath, 'utf8')
|
|
1223
|
+
} catch (err) {
|
|
1224
|
+
// Skip binary files
|
|
1225
|
+
continue
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
files.push({
|
|
1229
|
+
path: relativePath.replace(/\\/g, '/'),
|
|
1230
|
+
content: content
|
|
1231
|
+
})
|
|
1085
1232
|
}
|
|
1086
|
-
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
findFiles(skillDir, skillDir)
|
|
1237
|
+
|
|
1238
|
+
// Get existing files to delete
|
|
1239
|
+
const existingSkill = await axios.get(`${API_BASE_URL}/api/posts/${postId}`, {
|
|
1240
|
+
headers: {
|
|
1241
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
1242
|
+
'User-Agent': `skillscokac-cli/${VERSION}`
|
|
1243
|
+
},
|
|
1244
|
+
timeout: AXIOS_TIMEOUT
|
|
1245
|
+
})
|
|
1246
|
+
|
|
1247
|
+
const existingFiles = existingSkill.data.skillFiles || []
|
|
1248
|
+
|
|
1249
|
+
// Prepare batch payload
|
|
1250
|
+
const batchPayload = {
|
|
1251
|
+
skillMd: skillMdContent,
|
|
1252
|
+
files: {
|
|
1253
|
+
delete: existingFiles.map(f => ({ id: f.id })),
|
|
1254
|
+
create: files
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// Execute batch update
|
|
1259
|
+
const response = await axios.post(
|
|
1260
|
+
`${API_BASE_URL}/api/posts/${postId}/files/batch`,
|
|
1261
|
+
batchPayload,
|
|
1262
|
+
{
|
|
1263
|
+
headers: {
|
|
1264
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
1265
|
+
'Content-Type': 'application/json',
|
|
1266
|
+
'User-Agent': `skillscokac-cli/${VERSION}`
|
|
1267
|
+
},
|
|
1268
|
+
timeout: AXIOS_TIMEOUT
|
|
1269
|
+
}
|
|
1270
|
+
)
|
|
1271
|
+
|
|
1272
|
+
return {
|
|
1273
|
+
uploadedCount: files.length,
|
|
1274
|
+
deletedCount: existingFiles.length
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
/**
|
|
1279
|
+
* Upload or modify skill command handler
|
|
1280
|
+
*/
|
|
1281
|
+
async function uploadModifySkillCommand(skillDir, apiKey) {
|
|
1282
|
+
// Validate API key
|
|
1283
|
+
if (!apiKey) {
|
|
1284
|
+
console.log(chalk.red('✗ API key is required'))
|
|
1285
|
+
console.log(chalk.dim('Usage: npx skillscokac --uploadmodify <skillDir> --apikey <key>'))
|
|
1286
|
+
console.log()
|
|
1287
|
+
process.exit(1)
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// Resolve skill directory
|
|
1291
|
+
const resolvedSkillDir = path.resolve(skillDir)
|
|
1292
|
+
const skillMdPath = path.join(resolvedSkillDir, 'SKILL.md')
|
|
1293
|
+
|
|
1294
|
+
// Validate directory exists
|
|
1295
|
+
if (!fs.existsSync(resolvedSkillDir)) {
|
|
1296
|
+
console.log(chalk.red(`✗ Directory not found: ${resolvedSkillDir}`))
|
|
1087
1297
|
console.log()
|
|
1298
|
+
process.exit(1)
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// Validate SKILL.md exists
|
|
1302
|
+
if (!fs.existsSync(skillMdPath)) {
|
|
1303
|
+
console.log(chalk.red(`✗ SKILL.md not found in: ${resolvedSkillDir}`))
|
|
1304
|
+
console.log(chalk.dim(' The skill directory must contain a SKILL.md file'))
|
|
1305
|
+
console.log()
|
|
1306
|
+
process.exit(1)
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
let spinner
|
|
1310
|
+
try {
|
|
1311
|
+
// Step 1: Parse SKILL.md
|
|
1312
|
+
spinner = ora('Checking skill...').start()
|
|
1313
|
+
const skillMdContent = fs.readFileSync(skillMdPath, 'utf8')
|
|
1314
|
+
const { metadata } = parseFrontmatter(skillMdContent)
|
|
1315
|
+
|
|
1316
|
+
if (!metadata.name) {
|
|
1317
|
+
spinner.fail(chalk.red('SKILL.md must have a "name" in frontmatter'))
|
|
1318
|
+
process.exit(1)
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
if (!metadata.description) {
|
|
1322
|
+
spinner.fail(chalk.red('SKILL.md must have a "description" in frontmatter'))
|
|
1323
|
+
process.exit(1)
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
const skillName = metadata.name
|
|
1327
|
+
|
|
1328
|
+
// Step 2: Check if skill exists
|
|
1329
|
+
spinner.text = 'Checking if skill exists...'
|
|
1330
|
+
const existingSkill = await findSkillByName(skillName, apiKey)
|
|
1331
|
+
|
|
1332
|
+
if (existingSkill) {
|
|
1333
|
+
// Update existing skill
|
|
1334
|
+
console.log(chalk.yellow(`Skill "${skillName}" already exists. Updating...`))
|
|
1335
|
+
|
|
1336
|
+
spinner.text = 'Updating skill and files...'
|
|
1337
|
+
const { uploadedCount, deletedCount } = await updateSkillWithBatch(
|
|
1338
|
+
existingSkill.id,
|
|
1339
|
+
skillMdContent,
|
|
1340
|
+
resolvedSkillDir,
|
|
1341
|
+
apiKey,
|
|
1342
|
+
true
|
|
1343
|
+
)
|
|
1344
|
+
|
|
1345
|
+
spinner.succeed(chalk.green(`Updated: ${skillName} (${uploadedCount} files)`))
|
|
1346
|
+
console.log(chalk.dim(` Deleted ${deletedCount} old file${deletedCount !== 1 ? 's' : ''}, uploaded ${uploadedCount} new file${uploadedCount !== 1 ? 's' : ''}`))
|
|
1347
|
+
console.log(chalk.cyan(`https://skills.cokac.com/p/${existingSkill.id}`))
|
|
1348
|
+
} else {
|
|
1349
|
+
// Create new skill
|
|
1350
|
+
console.log(chalk.cyan(`Skill "${skillName}" does not exist. Creating new...`))
|
|
1351
|
+
|
|
1352
|
+
const skillData = {
|
|
1353
|
+
name: metadata.name,
|
|
1354
|
+
description: metadata.description,
|
|
1355
|
+
content: skillMdContent,
|
|
1356
|
+
visibility: 'PUBLIC',
|
|
1357
|
+
tags: ['claude-code', 'agent-skill']
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
spinner.text = 'Creating skill...'
|
|
1361
|
+
const skill = await createSkill(skillData, apiKey, true)
|
|
1362
|
+
|
|
1363
|
+
spinner.text = 'Uploading files...'
|
|
1364
|
+
const { uploadedCount } = await uploadSkillFiles(skill.id, resolvedSkillDir, apiKey, true)
|
|
1365
|
+
|
|
1366
|
+
const fileInfo = uploadedCount > 0 ? ` (${uploadedCount} file${uploadedCount !== 1 ? 's' : ''})` : ''
|
|
1367
|
+
spinner.succeed(chalk.green(`Created: ${skillData.name}${fileInfo}`))
|
|
1368
|
+
console.log(chalk.cyan(`https://skills.cokac.com/p/${skill.id}`))
|
|
1369
|
+
}
|
|
1088
1370
|
|
|
1089
1371
|
} catch (error) {
|
|
1090
|
-
|
|
1372
|
+
if (spinner) spinner.stop()
|
|
1373
|
+
|
|
1374
|
+
// Handle specific errors
|
|
1375
|
+
if (error.response) {
|
|
1376
|
+
if (error.response.status === 403) {
|
|
1377
|
+
console.log(chalk.red('✗ Forbidden: You do not have permission to modify this skill'))
|
|
1378
|
+
} else if (error.response.status === 401) {
|
|
1379
|
+
console.log(chalk.red('✗ Unauthorized: Invalid API key'))
|
|
1380
|
+
} else {
|
|
1381
|
+
console.error(chalk.red('✗ Upload/Update failed:'), error.response.data?.error || error.message)
|
|
1382
|
+
}
|
|
1383
|
+
} else {
|
|
1384
|
+
console.error(chalk.red('✗ Upload/Update failed:'), error.message)
|
|
1385
|
+
}
|
|
1091
1386
|
process.exit(1)
|
|
1092
1387
|
}
|
|
1093
1388
|
}
|
|
@@ -1107,6 +1402,7 @@ program
|
|
|
1107
1402
|
.option('-c, --install-collection <collectionId>', 'Install all skills from a collection')
|
|
1108
1403
|
.option('-d, --download <args...>', 'Download a skill to a directory (usage: --download <skillName> [path], defaults to current directory)')
|
|
1109
1404
|
.option('-u, --upload <skillDir>', 'Upload a skill from a directory (requires --apikey)')
|
|
1405
|
+
.option('-m, --uploadmodify <skillDir>', 'Upload or update a skill (creates if new, updates if exists, requires --apikey)')
|
|
1110
1406
|
.option('--apikey <key>', 'API key for uploading skills')
|
|
1111
1407
|
.option('-r, --remove-skill <skillName>', 'Remove an installed skill')
|
|
1112
1408
|
.option('-f, --remove-skill-force <skillName>', 'Remove skill from all locations without confirmation')
|
|
@@ -1136,6 +1432,8 @@ const options = program.opts()
|
|
|
1136
1432
|
await downloadSkillCommand(skillName, downloadPath)
|
|
1137
1433
|
} else if (options.upload) {
|
|
1138
1434
|
await uploadSkillCommand(options.upload, options.apikey)
|
|
1435
|
+
} else if (options.uploadmodify) {
|
|
1436
|
+
await uploadModifySkillCommand(options.uploadmodify, options.apikey)
|
|
1139
1437
|
} else if (options.removeAllSkillsForce) {
|
|
1140
1438
|
await removeAllSkillsCommand(true)
|
|
1141
1439
|
} else if (options.removeAllSkills) {
|