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 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
- | `--apikey <key>` | API key for uploading skills |
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
- ### Example
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
@@ -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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillscokac",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
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": {