skillscokac 1.3.0 → 1.4.1
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 +231 -0
- 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
|
@@ -1159,6 +1159,234 @@ async function uploadSkillCommand(skillDir, apiKey) {
|
|
|
1159
1159
|
}
|
|
1160
1160
|
}
|
|
1161
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
|
+
})
|
|
1232
|
+
}
|
|
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}`))
|
|
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
|
+
}
|
|
1370
|
+
|
|
1371
|
+
} catch (error) {
|
|
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
|
+
}
|
|
1386
|
+
process.exit(1)
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1162
1390
|
/**
|
|
1163
1391
|
* Setup CLI with Commander
|
|
1164
1392
|
*/
|
|
@@ -1174,6 +1402,7 @@ program
|
|
|
1174
1402
|
.option('-c, --install-collection <collectionId>', 'Install all skills from a collection')
|
|
1175
1403
|
.option('-d, --download <args...>', 'Download a skill to a directory (usage: --download <skillName> [path], defaults to current directory)')
|
|
1176
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)')
|
|
1177
1406
|
.option('--apikey <key>', 'API key for uploading skills')
|
|
1178
1407
|
.option('-r, --remove-skill <skillName>', 'Remove an installed skill')
|
|
1179
1408
|
.option('-f, --remove-skill-force <skillName>', 'Remove skill from all locations without confirmation')
|
|
@@ -1203,6 +1432,8 @@ const options = program.opts()
|
|
|
1203
1432
|
await downloadSkillCommand(skillName, downloadPath)
|
|
1204
1433
|
} else if (options.upload) {
|
|
1205
1434
|
await uploadSkillCommand(options.upload, options.apikey)
|
|
1435
|
+
} else if (options.uploadmodify) {
|
|
1436
|
+
await uploadModifySkillCommand(options.uploadmodify, options.apikey)
|
|
1206
1437
|
} else if (options.removeAllSkillsForce) {
|
|
1207
1438
|
await removeAllSkillsCommand(true)
|
|
1208
1439
|
} else if (options.removeAllSkills) {
|