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 +119 -5
- package/bin/skillscokac.js +410 -3
- package/package.json +3 -3
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
|
-
|
|
|
172
|
-
|
|
|
173
|
-
|
|
|
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
|
|
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)
|
|
@@ -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.
|
|
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": "
|
|
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"
|