skillscokac 1.0.0 → 1.1.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
 
@@ -675,6 +675,83 @@ async function removeAllSkillsCommand(force = false) {
675
675
  }
676
676
  }
677
677
 
678
+ /**
679
+ * Download skill command handler
680
+ */
681
+ async function downloadSkillCommand(skillName, downloadPath) {
682
+ // Use current directory if no path specified
683
+ if (!downloadPath) {
684
+ downloadPath = process.cwd()
685
+ }
686
+
687
+ // Fetch skill
688
+ const skill = await fetchSkill(skillName)
689
+
690
+ // Display skill info
691
+ displaySkillInfo(skill)
692
+ console.log()
693
+
694
+ // Resolve download path
695
+ const resolvedPath = path.resolve(downloadPath)
696
+ const targetDir = path.join(resolvedPath, skill.skillName)
697
+
698
+ const spinner = ora('Downloading skill...').start()
699
+
700
+ try {
701
+ // Create target directory
702
+ if (fs.existsSync(targetDir)) {
703
+ spinner.text = 'Removing existing directory...'
704
+ fs.rmSync(targetDir, { recursive: true, force: true })
705
+ }
706
+
707
+ spinner.text = 'Creating directory...'
708
+ fs.mkdirSync(targetDir, { recursive: true })
709
+
710
+ // Write SKILL.md
711
+ spinner.text = 'Writing files...'
712
+ const skillMdPath = path.join(targetDir, 'SKILL.md')
713
+ fs.writeFileSync(skillMdPath, skill.skillMd || '', 'utf8')
714
+
715
+ // Write additional files
716
+ if (skill.files && skill.files.length > 0) {
717
+ for (const file of skill.files) {
718
+ // Security: Prevent path traversal attacks
719
+ const normalizedPath = path.normalize(file.path).replace(/^(\.\.(\/|\\|$))+/, '')
720
+ const filePath = path.join(targetDir, normalizedPath)
721
+
722
+ // Verify that the final path is still within targetDir
723
+ const resolvedFilePath = path.resolve(filePath)
724
+ const resolvedTargetDir = path.resolve(targetDir)
725
+
726
+ if (!resolvedFilePath.startsWith(resolvedTargetDir + path.sep) && resolvedFilePath !== resolvedTargetDir) {
727
+ console.warn(chalk.yellow(`⚠ Skipping potentially unsafe file path: ${file.path}`))
728
+ continue
729
+ }
730
+
731
+ const fileDir = path.dirname(filePath)
732
+
733
+ // Create subdirectories if needed
734
+ if (!fs.existsSync(fileDir)) {
735
+ fs.mkdirSync(fileDir, { recursive: true })
736
+ }
737
+
738
+ fs.writeFileSync(filePath, file.content || '', 'utf8')
739
+ }
740
+ }
741
+
742
+ spinner.succeed(chalk.green('Downloaded successfully!'))
743
+ console.log()
744
+ console.log(chalk.dim(' Location: ') + chalk.cyan(targetDir))
745
+ console.log(chalk.dim(' Files: ') + chalk.white(`${1 + (skill.files ? skill.files.length : 0)} file${skill.files && skill.files.length !== 0 ? 's' : ''}`))
746
+ console.log()
747
+
748
+ } catch (error) {
749
+ spinner.fail(chalk.red('Failed to download skill'))
750
+ console.error(chalk.red('\nError:'), error.message)
751
+ process.exit(1)
752
+ }
753
+ }
754
+
678
755
  /**
679
756
  * List installed skills command handler
680
757
  */
@@ -767,6 +844,254 @@ async function listInstalledSkillsCommand() {
767
844
  }
768
845
  }
769
846
 
847
+ /**
848
+ * Create skill on SkillsCokac API
849
+ */
850
+ async function createSkill(skillData, apiKey) {
851
+ const payload = {
852
+ type: 'SKILL',
853
+ title: skillData.name,
854
+ description: skillData.description,
855
+ content: skillData.content,
856
+ skillMd: skillData.content,
857
+ visibility: skillData.visibility || 'PUBLIC',
858
+ tags: skillData.tags || ['claude-code', 'agent-skill']
859
+ }
860
+
861
+ const spinner = ora('Creating skill on SkillsCokac...').start()
862
+
863
+ try {
864
+ const response = await axios.post(`${API_BASE_URL}/api/posts`, payload, {
865
+ headers: {
866
+ 'Authorization': `Bearer ${apiKey}`,
867
+ 'Content-Type': 'application/json',
868
+ 'User-Agent': `skillscokac-cli/${VERSION}`
869
+ }
870
+ })
871
+
872
+ spinner.succeed(chalk.green('Skill created successfully!'))
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
879
+ } catch (error) {
880
+ spinner.fail(chalk.red('Failed to create skill'))
881
+ if (error.response) {
882
+ console.error(chalk.red(' Status:'), error.response.status)
883
+ console.error(chalk.red(' Response:'), error.response.data)
884
+ } else {
885
+ console.error(chalk.red(' Error:'), error.message)
886
+ }
887
+ throw error
888
+ }
889
+ }
890
+
891
+ /**
892
+ * Upload skill files to SkillsCokac API
893
+ */
894
+ async function uploadSkillFiles(skillId, skillDir, apiKey) {
895
+ const spinner = ora('Uploading additional files...').start()
896
+
897
+ let uploadedCount = 0
898
+ let failedCount = 0
899
+ const files = []
900
+
901
+ // Recursively find all files in the directory
902
+ function findFiles(dir, baseDir) {
903
+ const entries = fs.readdirSync(dir, { withFileTypes: true })
904
+
905
+ for (const entry of entries) {
906
+ const fullPath = path.join(dir, entry.name)
907
+
908
+ if (entry.isDirectory()) {
909
+ // Skip hidden directories and common ignore patterns
910
+ if (entry.name.startsWith('.') || entry.name === '__pycache__' || entry.name === 'node_modules') {
911
+ continue
912
+ }
913
+ findFiles(fullPath, baseDir)
914
+ } else if (entry.isFile()) {
915
+ // Skip SKILL.md at root level (already uploaded as main content)
916
+ const relativePath = path.relative(baseDir, fullPath)
917
+ if (relativePath === 'SKILL.md') {
918
+ continue
919
+ }
920
+
921
+ // Skip hidden files
922
+ if (entry.name.startsWith('.')) {
923
+ continue
924
+ }
925
+
926
+ files.push({ fullPath, relativePath })
927
+ }
928
+ }
929
+ }
930
+
931
+ try {
932
+ findFiles(skillDir, skillDir)
933
+
934
+ if (files.length === 0) {
935
+ spinner.succeed(chalk.green('No additional files to upload'))
936
+ return
937
+ }
938
+
939
+ spinner.text = `Found ${files.length} file${files.length !== 1 ? 's' : ''} to upload...`
940
+
941
+ for (const file of files) {
942
+ try {
943
+ // Try to read as text
944
+ let content
945
+ try {
946
+ content = fs.readFileSync(file.fullPath, 'utf8')
947
+ } catch (err) {
948
+ // Skip binary files
949
+ spinner.warn(chalk.yellow(` Skipping binary file: ${file.relativePath}`))
950
+ continue
951
+ }
952
+
953
+ const filePayload = {
954
+ path: file.relativePath.replace(/\\/g, '/'), // Normalize Windows paths
955
+ content: content
956
+ }
957
+
958
+ spinner.text = `Uploading: ${file.relativePath}`
959
+
960
+ const response = await axios.post(
961
+ `${API_BASE_URL}/api/posts/${skillId}/files`,
962
+ filePayload,
963
+ {
964
+ headers: {
965
+ 'Authorization': `Bearer ${apiKey}`,
966
+ 'Content-Type': 'application/json',
967
+ 'User-Agent': `skillscokac-cli/${VERSION}`
968
+ }
969
+ }
970
+ )
971
+
972
+ if (response.status === 201) {
973
+ uploadedCount++
974
+ } else {
975
+ failedCount++
976
+ }
977
+ } catch (error) {
978
+ 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
+ }
984
+ }
985
+
986
+ if (failedCount === 0) {
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
+
993
+ } catch (error) {
994
+ spinner.fail(chalk.red('Failed to upload files'))
995
+ throw error
996
+ }
997
+ }
998
+
999
+ /**
1000
+ * Upload skill command handler
1001
+ */
1002
+ async function uploadSkillCommand(skillDir, apiKey) {
1003
+ // Validate API key
1004
+ if (!apiKey) {
1005
+ console.log(chalk.red('✗ API key is required'))
1006
+ console.log(chalk.dim('Usage: npx skillscokac --upload <skillDir> --apikey <key>'))
1007
+ console.log()
1008
+ process.exit(1)
1009
+ }
1010
+
1011
+ // Resolve skill directory
1012
+ const resolvedSkillDir = path.resolve(skillDir)
1013
+ const skillMdPath = path.join(resolvedSkillDir, 'SKILL.md')
1014
+
1015
+ // Validate directory exists
1016
+ if (!fs.existsSync(resolvedSkillDir)) {
1017
+ console.log(chalk.red(`✗ Directory not found: ${resolvedSkillDir}`))
1018
+ console.log()
1019
+ process.exit(1)
1020
+ }
1021
+
1022
+ // Validate SKILL.md exists
1023
+ if (!fs.existsSync(skillMdPath)) {
1024
+ console.log(chalk.red(`✗ SKILL.md not found in: ${resolvedSkillDir}`))
1025
+ console.log(chalk.dim(' The skill directory must contain a SKILL.md file'))
1026
+ console.log()
1027
+ process.exit(1)
1028
+ }
1029
+
1030
+ console.log(boxen(
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
+
1042
+ try {
1043
+ // Step 1: Parse SKILL.md
1044
+ const spinner = ora('Parsing SKILL.md...').start()
1045
+ const skillMdContent = fs.readFileSync(skillMdPath, 'utf8')
1046
+ const { metadata } = parseFrontmatter(skillMdContent)
1047
+
1048
+ if (!metadata.name) {
1049
+ spinner.fail(chalk.red('SKILL.md must have a "name" in frontmatter'))
1050
+ process.exit(1)
1051
+ }
1052
+
1053
+ if (!metadata.description) {
1054
+ spinner.fail(chalk.red('SKILL.md must have a "description" in frontmatter'))
1055
+ process.exit(1)
1056
+ }
1057
+
1058
+ const skillData = {
1059
+ name: metadata.name,
1060
+ description: metadata.description,
1061
+ content: skillMdContent,
1062
+ visibility: 'PUBLIC',
1063
+ tags: ['claude-code', 'agent-skill']
1064
+ }
1065
+
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
+ // Step 2: Create skill
1072
+ const skill = await createSkill(skillData, apiKey)
1073
+
1074
+ // Step 3: Upload additional files
1075
+ await uploadSkillFiles(skill.id, resolvedSkillDir, apiKey)
1076
+
1077
+ // Success summary
1078
+ console.log(boxen(
1079
+ chalk.bold.green('✓ Upload complete!\n\n') +
1080
+ chalk.dim('Skill URL: ') + chalk.cyan(`https://skills.cokac.com/s/${skill.id}`),
1081
+ {
1082
+ padding: { top: 0, bottom: 0, left: 1, right: 1 },
1083
+ borderStyle: 'round',
1084
+ borderColor: 'green'
1085
+ }
1086
+ ))
1087
+ console.log()
1088
+
1089
+ } catch (error) {
1090
+ console.error(chalk.red('\n✗ Upload failed:'), error.message)
1091
+ process.exit(1)
1092
+ }
1093
+ }
1094
+
770
1095
  /**
771
1096
  * Setup CLI with Commander
772
1097
  */
@@ -780,6 +1105,9 @@ program
780
1105
  program
781
1106
  .option('-i, --install-skill <skillName>', 'Install a skill by name')
782
1107
  .option('-c, --install-collection <collectionId>', 'Install all skills from a collection')
1108
+ .option('-d, --download <args...>', 'Download a skill to a directory (usage: --download <skillName> [path], defaults to current directory)')
1109
+ .option('-u, --upload <skillDir>', 'Upload a skill from a directory (requires --apikey)')
1110
+ .option('--apikey <key>', 'API key for uploading skills')
783
1111
  .option('-r, --remove-skill <skillName>', 'Remove an installed skill')
784
1112
  .option('-f, --remove-skill-force <skillName>', 'Remove skill from all locations without confirmation')
785
1113
  .option('-a, --remove-all-skills', 'Remove all installed skills')
@@ -796,6 +1124,18 @@ const options = program.opts()
796
1124
  await installSkillCommand(options.installSkill)
797
1125
  } else if (options.installCollection) {
798
1126
  await installCollectionCommand(options.installCollection)
1127
+ } else if (options.download) {
1128
+ // Download expects one or two arguments: skillName and optional path
1129
+ if (options.download.length < 1 || options.download.length > 2) {
1130
+ console.log(chalk.red('✗ Invalid arguments for --download'))
1131
+ console.log(chalk.dim('Usage: npx skillscokac --download <skillName> [path]'))
1132
+ console.log()
1133
+ process.exit(1)
1134
+ }
1135
+ const [skillName, downloadPath] = options.download
1136
+ await downloadSkillCommand(skillName, downloadPath)
1137
+ } else if (options.upload) {
1138
+ await uploadSkillCommand(options.upload, options.apikey)
799
1139
  } else if (options.removeAllSkillsForce) {
800
1140
  await removeAllSkillsCommand(true)
801
1141
  } 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.1.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"