skill-market-cli 1.0.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 ADDED
@@ -0,0 +1,121 @@
1
+ # Skill Market CLI
2
+
3
+ A command-line tool for managing skills on Skill Market.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g skill-market-cli
9
+ # or
10
+ npx skill-market-cli
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```bash
16
+ # Login
17
+ skill-market-cli login
18
+
19
+ # List all skills
20
+ skill-market-cli list
21
+
22
+ # List your skills
23
+ skill-market-cli list --my
24
+
25
+ # Upload a new skill
26
+ skill-market-cli upload ./my-skill
27
+
28
+ # Collect AI responses for examples
29
+ skill-market-cli run-example ./my-skill --model claude-3-5-sonnet
30
+
31
+ # Update a skill
32
+ skill-market-cli update <skill-id> --file ./my-skill/SKILL.md
33
+
34
+ # Delete a skill
35
+ skill-market-cli delete <skill-id>
36
+ ```
37
+
38
+ ## Commands
39
+
40
+ ### `login`
41
+
42
+ Login to Skill Market using OAuth.
43
+
44
+ ```bash
45
+ skill-market-cli login
46
+ ```
47
+
48
+ ### `logout`
49
+
50
+ Logout and revoke access token.
51
+
52
+ ```bash
53
+ skill-market-cli logout
54
+ ```
55
+
56
+ ### `list`
57
+
58
+ List skills on the market.
59
+
60
+ ```bash
61
+ skill-market-cli list
62
+ skill-market-cli list --my # Show only your skills
63
+ skill-market-cli list --json # Output as JSON
64
+ skill-market-cli list -p 2 -s 10 # Page 2, 10 items per page
65
+ ```
66
+
67
+ ### `upload`
68
+
69
+ Upload a new skill.
70
+
71
+ ```bash
72
+ skill-market-cli upload ./my-skill
73
+ skill-market-cli upload ./my-skill/SKILL.md --name "my-skill" --tags "tag1,tag2"
74
+ ```
75
+
76
+ ### `run-example`
77
+
78
+ Run user examples and collect AI responses.
79
+
80
+ ```bash
81
+ skill-market-cli run-example ./my-skill --model claude-3-5-sonnet
82
+ ```
83
+
84
+ This command:
85
+ 1. Reads your SKILL.md
86
+ 2. Runs each example through the specified AI model
87
+ 3. Collects thinking steps, tool calls, and messages
88
+ 4. Saves to `.skill-examples.json`
89
+
90
+ ### `update`
91
+
92
+ Update an existing skill.
93
+
94
+ ```bash
95
+ skill-market-cli update <skill-id> --file ./my-skill/SKILL.md
96
+ ```
97
+
98
+ ### `delete`
99
+
100
+ Delete a skill.
101
+
102
+ ```bash
103
+ skill-market-cli delete <skill-id>
104
+ skill-market-cli delete <skill-id> --force # Skip confirmation
105
+ ```
106
+
107
+ ### `guide`
108
+
109
+ Show skill upload guide.
110
+
111
+ ```bash
112
+ skill-market-cli guide
113
+ ```
114
+
115
+ ## Configuration
116
+
117
+ Configuration is stored in `~/.skill-market-cli/config.json`.
118
+
119
+ ## License
120
+
121
+ MIT
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+
3
+ const path = require('path');
4
+ require(path.join(__dirname, '..', 'src', 'index.js'));
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "skill-market-cli",
3
+ "version": "1.0.0",
4
+ "description": "CLI tool for managing skills on Skill Market",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "skill-market-cli": "./bin/skill-market-cli.js",
8
+ "smcli": "./bin/skill-market-cli.js"
9
+ },
10
+ "scripts": {
11
+ "test": "echo \"Error: no test specified\" && exit 1"
12
+ },
13
+ "keywords": [
14
+ "skill",
15
+ "cli",
16
+ "market",
17
+ "ai",
18
+ "kimi",
19
+ "claude",
20
+ "cursor"
21
+ ],
22
+ "author": "Kirigaya",
23
+ "license": "MIT",
24
+ "dependencies": {
25
+ "axios": "^1.6.0",
26
+ "chalk": "^4.1.2",
27
+ "commander": "^11.0.0",
28
+ "fs-extra": "^11.1.1",
29
+ "inquirer": "^8.2.6",
30
+ "open": "^8.4.2",
31
+ "yaml": "^2.3.4"
32
+ },
33
+ "engines": {
34
+ "node": ">=14.0.0"
35
+ },
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/yourusername/skill-market-cli.git"
39
+ },
40
+ "homepage": "https://kirigaya.cn/ktools/skillmanager",
41
+ "bugs": {
42
+ "url": "https://github.com/yourusername/skill-market-cli/issues"
43
+ }
44
+ }
@@ -0,0 +1,132 @@
1
+ const axios = require('axios');
2
+ const chalk = require('chalk');
3
+ const { getToken, isLoggedIn, getServerConfig } = require('../auth/token-store');
4
+ const { refreshAccessToken } = require('../auth/oauth');
5
+
6
+ class ApiClient {
7
+ constructor() {
8
+ this.client = null;
9
+ this.init();
10
+ }
11
+
12
+ init() {
13
+ const serverConfig = getServerConfig();
14
+ this.client = axios.create({
15
+ baseURL: serverConfig.apiBase,
16
+ timeout: 30000,
17
+ headers: {
18
+ 'Content-Type': 'application/json'
19
+ }
20
+ });
21
+
22
+ // 请求拦截器 - 添加 Token
23
+ this.client.interceptors.request.use(
24
+ async (config) => {
25
+ if (isLoggedIn()) {
26
+ const { accessToken, expiresAt } = getToken();
27
+
28
+ // 检查 Token 是否即将过期
29
+ if (expiresAt && Date.now() > expiresAt - 60000) {
30
+ // Token 即将过期,尝试刷新
31
+ try {
32
+ const newToken = await refreshAccessToken();
33
+ config.headers['Authorization'] = `Bearer ${newToken}`;
34
+ } catch (e) {
35
+ // 刷新失败,使用现有 Token
36
+ config.headers['Authorization'] = `Bearer ${accessToken}`;
37
+ }
38
+ } else {
39
+ config.headers['Authorization'] = `Bearer ${accessToken}`;
40
+ }
41
+ }
42
+ return config;
43
+ },
44
+ (error) => Promise.reject(error)
45
+ );
46
+
47
+ // 响应拦截器 - 处理错误
48
+ this.client.interceptors.response.use(
49
+ (response) => response,
50
+ async (error) => {
51
+ if (error.response) {
52
+ // 401 未授权 - Token 可能已过期
53
+ if (error.response.status === 401) {
54
+ console.error(chalk.red('\n❌ Authentication expired. Please login again.'));
55
+ console.error(chalk.gray('Run: skill-market-cli login\n'));
56
+ }
57
+
58
+ // 403 禁止访问
59
+ if (error.response.status === 403) {
60
+ console.error(chalk.red('\n❌ You do not have permission to perform this action.'));
61
+ }
62
+
63
+ // 429 请求过多
64
+ if (error.response.status === 429) {
65
+ console.error(chalk.yellow('\n⚠️ Too many requests. Please try again later.'));
66
+ }
67
+ } else if (error.request) {
68
+ console.error(chalk.red('\n❌ Network error. Please check your connection.'));
69
+ }
70
+ return Promise.reject(error);
71
+ }
72
+ );
73
+ }
74
+
75
+ // Skills API
76
+ async listSkills(page = 1, pageSize = 20) {
77
+ const response = await this.client.get('/skill/list', {
78
+ params: { page, pageSize }
79
+ });
80
+ return response.data;
81
+ }
82
+
83
+ async getMySkills() {
84
+ // 获取当前用户的 skills
85
+ const response = await this.client.get('/skill/list', {
86
+ params: { page: 1, pageSize: 1000 }
87
+ });
88
+
89
+ const { user } = getToken();
90
+ if (response.data && response.data.data && response.data.data.list) {
91
+ response.data.data.list = response.data.data.list.filter(
92
+ skill => skill.creator === user.name
93
+ );
94
+ }
95
+ return response.data;
96
+ }
97
+
98
+ async getSkillDetail(id) {
99
+ const response = await this.client.get(`/skill/detail/${id}`);
100
+ return response.data;
101
+ }
102
+
103
+ async uploadSkill(data) {
104
+ const response = await this.client.post('/skill/upload', data);
105
+ return response.data;
106
+ }
107
+
108
+ async updateSkill(id, data) {
109
+ const response = await this.client.post(`/skill/update/${id}`, data);
110
+ return response.data;
111
+ }
112
+
113
+ async deleteSkill(id) {
114
+ const response = await this.client.post(`/skill/delete/${id}`);
115
+ return response.data;
116
+ }
117
+
118
+ // OAuth API
119
+ async getUserInfo() {
120
+ const serverConfig = getServerConfig();
121
+ const { accessToken } = getToken();
122
+ const response = await axios.get(`${serverConfig.baseURL}/oauth/userinfo`, {
123
+ headers: {
124
+ 'Authorization': `Bearer ${accessToken}`
125
+ }
126
+ });
127
+ return response.data;
128
+ }
129
+ }
130
+
131
+ // 导出单例
132
+ module.exports = new ApiClient();
@@ -0,0 +1,219 @@
1
+ const http = require('http');
2
+ const url = require('url');
3
+ const chalk = require('chalk');
4
+ const open = require('open');
5
+ const axios = require('axios');
6
+ const { saveToken, getServerConfig, setServerConfig } = require('./token-store');
7
+
8
+ const DEFAULT_SERVER_URL = 'https://kirigaya.cn';
9
+ const CLIENT_ID = 'skill-market-cli';
10
+
11
+ // 获取 OAuth 配置
12
+ async function getOAuthConfig() {
13
+ try {
14
+ const serverConfig = getServerConfig();
15
+ const response = await axios.get(`${serverConfig.baseURL}/oauth/config`);
16
+ if (response.data && response.data.code === 200) {
17
+ return response.data.data;
18
+ }
19
+ } catch (e) {
20
+ // 使用默认配置
21
+ }
22
+ return {
23
+ clientId: CLIENT_ID,
24
+ redirectUri: 'http://localhost:0/callback',
25
+ authorizeURL: `${DEFAULT_SERVER_URL}/oauth/authorize`,
26
+ tokenURL: `${DEFAULT_SERVER_URL}/oauth/token`,
27
+ scopes: ['skill:read', 'skill:write', 'user:read']
28
+ };
29
+ }
30
+
31
+ // 启动本地回调服务器
32
+ function startCallbackServer(port = 0) {
33
+ return new Promise((resolve, reject) => {
34
+ const server = http.createServer((req, res) => {
35
+ const parsedUrl = url.parse(req.url, true);
36
+ const { code, error, error_description } = parsedUrl.query;
37
+
38
+ if (error) {
39
+ res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
40
+ res.end(`
41
+ <!DOCTYPE html>
42
+ <html>
43
+ <head><title>授权失败</title></head>
44
+ <body style="text-align:center;padding:50px;font-family:sans-serif;">
45
+ <h1 style="color:#f44336;">❌ 授权失败</h1>
46
+ <p>${error_description || error}</p>
47
+ <p>请关闭此窗口并返回命令行</p>
48
+ </body>
49
+ </html>
50
+ `);
51
+ server.close();
52
+ reject(new Error(error_description || error));
53
+ return;
54
+ }
55
+
56
+ if (code) {
57
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
58
+ res.end(`
59
+ <!DOCTYPE html>
60
+ <html>
61
+ <head><title>授权成功</title></head>
62
+ <body style="text-align:center;padding:50px;font-family:sans-serif;background:linear-gradient(135deg,#667eea,#764ba2);">
63
+ <div style="background:white;padding:40px;border-radius:12px;box-shadow:0 10px 40px rgba(0,0,0,0.2);display:inline-block;">
64
+ <div style="font-size:64px;color:#4CAF50;">✓</div>
65
+ <h1 style="color:#333;">授权成功</h1>
66
+ <p style="color:#666;">您已成功授权 Skill Market CLI</p>
67
+ <p style="color:#999;font-size:14px;">请关闭此窗口并返回命令行</p>
68
+ </div>
69
+ </body>
70
+ </html>
71
+ `);
72
+ server.close();
73
+ resolve(code);
74
+ }
75
+ });
76
+
77
+ server.listen(port, () => {
78
+ const actualPort = server.address().port;
79
+ console.log(chalk.gray(`Callback server listening on port ${actualPort}`));
80
+ });
81
+
82
+ server.on('error', reject);
83
+ });
84
+ }
85
+
86
+ // 执行 OAuth 登录流程
87
+ async function login(options = {}) {
88
+ console.log(chalk.blue('🔐 Starting OAuth login flow...\n'));
89
+
90
+ // 获取 OAuth 配置
91
+ const oauthConfig = await getOAuthConfig();
92
+ console.log(chalk.gray(`Server: ${oauthConfig.authorizeURL}`));
93
+
94
+ // 启动本地回调服务器
95
+ let callbackPort = 0;
96
+ const callbackPromise = startCallbackServer(callbackPort);
97
+
98
+ // 构造授权 URL
99
+ const state = generateRandomString(16);
100
+ const redirectUri = `http://localhost:${callbackPort}/callback`;
101
+
102
+ const authUrl = new URL(oauthConfig.authorizeURL);
103
+ authUrl.searchParams.set('response_type', 'code');
104
+ authUrl.searchParams.set('client_id', oauthConfig.clientId);
105
+ authUrl.searchParams.set('redirect_uri', redirectUri);
106
+ authUrl.searchParams.set('scope', oauthConfig.scopes.join(' '));
107
+ authUrl.searchParams.set('state', state);
108
+
109
+ console.log(chalk.cyan('\n📱 Please authorize the CLI in your browser.\n'));
110
+ console.log(chalk.gray('Authorization URL:'));
111
+ console.log(chalk.underline(authUrl.toString()));
112
+ console.log();
113
+
114
+ // 自动打开浏览器
115
+ if (options.open !== false) {
116
+ try {
117
+ await open(authUrl.toString());
118
+ console.log(chalk.gray('Browser opened automatically.\n'));
119
+ } catch (e) {
120
+ console.log(chalk.yellow('Could not open browser automatically.'));
121
+ console.log(chalk.yellow('Please open the URL manually.\n'));
122
+ }
123
+ }
124
+
125
+ // 等待回调
126
+ try {
127
+ const code = await callbackPromise;
128
+ console.log(chalk.green('✅ Authorization code received'));
129
+
130
+ // 交换 Token
131
+ console.log(chalk.gray('Exchanging code for token...'));
132
+
133
+ const tokenResponse = await axios.post(oauthConfig.tokenURL, {
134
+ grant_type: 'authorization_code',
135
+ code: code,
136
+ redirect_uri: redirectUri,
137
+ client_id: oauthConfig.clientId,
138
+ client_secret: 'dummy-secret' // 实际使用时需要从安全的地方获取
139
+ });
140
+
141
+ const { access_token, refresh_token, expires_in } = tokenResponse.data;
142
+
143
+ // 获取用户信息
144
+ const userResponse = await axios.get(`${oauthConfig.authorizeURL.replace('/authorize', '/userinfo')}`, {
145
+ headers: {
146
+ 'Authorization': `Bearer ${access_token}`
147
+ }
148
+ });
149
+
150
+ const user = userResponse.data;
151
+
152
+ // 保存 Token
153
+ const expiresAt = Date.now() + (expires_in * 1000);
154
+ saveToken(access_token, refresh_token, expiresAt, {
155
+ name: user.name,
156
+ email: user.email,
157
+ picture: user.picture
158
+ });
159
+
160
+ console.log();
161
+ console.log(chalk.green('✅ Login successful!'));
162
+ console.log(chalk.cyan(`👤 Welcome, ${user.name}!`));
163
+ console.log();
164
+
165
+ return true;
166
+ } catch (error) {
167
+ console.error();
168
+ console.error(chalk.red('❌ Login failed:'), error.message);
169
+ if (error.response) {
170
+ console.error(chalk.red('Server response:'), error.response.data);
171
+ }
172
+ return false;
173
+ }
174
+ }
175
+
176
+ // 刷新 Token
177
+ async function refreshAccessToken() {
178
+ const { refreshToken } = require('./token-store').getToken();
179
+ if (!refreshToken) {
180
+ throw new Error('No refresh token available');
181
+ }
182
+
183
+ const oauthConfig = await getOAuthConfig();
184
+
185
+ try {
186
+ const response = await axios.post(oauthConfig.tokenURL, {
187
+ grant_type: 'refresh_token',
188
+ refresh_token: refreshToken,
189
+ client_id: oauthConfig.clientId,
190
+ client_secret: 'dummy-secret'
191
+ });
192
+
193
+ const { access_token, refresh_token, expires_in } = response.data;
194
+ const expiresAt = Date.now() + (expires_in * 1000);
195
+
196
+ const { user } = require('./token-store').getToken();
197
+ saveToken(access_token, refresh_token, expiresAt, user);
198
+
199
+ return access_token;
200
+ } catch (error) {
201
+ throw new Error('Failed to refresh token: ' + error.message);
202
+ }
203
+ }
204
+
205
+ // 生成随机字符串
206
+ function generateRandomString(length) {
207
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
208
+ let result = '';
209
+ for (let i = 0; i < length; i++) {
210
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
211
+ }
212
+ return result;
213
+ }
214
+
215
+ module.exports = {
216
+ login,
217
+ refreshAccessToken,
218
+ getOAuthConfig
219
+ };
@@ -0,0 +1,100 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ const CONFIG_DIR = path.join(os.homedir(), '.skill-market-cli');
6
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
7
+
8
+ // 确保配置目录存在
9
+ function ensureConfigDir() {
10
+ fs.ensureDirSync(CONFIG_DIR);
11
+ }
12
+
13
+ // 获取配置
14
+ function getConfig() {
15
+ ensureConfigDir();
16
+ if (fs.existsSync(CONFIG_FILE)) {
17
+ try {
18
+ return fs.readJsonSync(CONFIG_FILE);
19
+ } catch (e) {
20
+ return {};
21
+ }
22
+ }
23
+ return {};
24
+ }
25
+
26
+ // 保存配置
27
+ function saveConfig(config) {
28
+ ensureConfigDir();
29
+ fs.writeJsonSync(CONFIG_FILE, config, { spaces: 2 });
30
+ }
31
+
32
+ // 保存 Token
33
+ function saveToken(accessToken, refreshToken, expiresAt, user) {
34
+ const config = getConfig();
35
+ config.accessToken = accessToken;
36
+ config.refreshToken = refreshToken;
37
+ config.expiresAt = expiresAt;
38
+ config.user = user;
39
+ saveConfig(config);
40
+ }
41
+
42
+ // 获取 Token
43
+ function getToken() {
44
+ const config = getConfig();
45
+ return {
46
+ accessToken: config.accessToken,
47
+ refreshToken: config.refreshToken,
48
+ expiresAt: config.expiresAt,
49
+ user: config.user
50
+ };
51
+ }
52
+
53
+ // 清除 Token
54
+ function clearToken() {
55
+ const config = getConfig();
56
+ delete config.accessToken;
57
+ delete config.refreshToken;
58
+ delete config.expiresAt;
59
+ delete config.user;
60
+ saveConfig(config);
61
+ }
62
+
63
+ // 检查是否已登录
64
+ function isLoggedIn() {
65
+ const config = getConfig();
66
+ if (!config.accessToken) return false;
67
+ if (config.expiresAt && Date.now() > config.expiresAt) {
68
+ return false;
69
+ }
70
+ return true;
71
+ }
72
+
73
+ // 获取服务器配置
74
+ function getServerConfig() {
75
+ const config = getConfig();
76
+ return config.server || {
77
+ baseURL: 'https://kirigaya.cn',
78
+ apiBase: 'https://kirigaya.cn/api'
79
+ };
80
+ }
81
+
82
+ // 保存服务器配置
83
+ function setServerConfig(serverConfig) {
84
+ const config = getConfig();
85
+ config.server = serverConfig;
86
+ saveConfig(config);
87
+ }
88
+
89
+ module.exports = {
90
+ getConfig,
91
+ saveConfig,
92
+ saveToken,
93
+ getToken,
94
+ clearToken,
95
+ isLoggedIn,
96
+ getServerConfig,
97
+ setServerConfig,
98
+ CONFIG_DIR,
99
+ CONFIG_FILE
100
+ };
@@ -0,0 +1,63 @@
1
+ const chalk = require('chalk');
2
+ const inquirer = require('inquirer');
3
+ const { isLoggedIn } = require('../auth/token-store');
4
+ const apiClient = require('../api/client');
5
+
6
+ async function remove(skillId, options) {
7
+ if (!isLoggedIn()) {
8
+ console.error(chalk.red('❌ Please login first: skill-market-cli login\n'));
9
+ process.exit(1);
10
+ }
11
+
12
+ try {
13
+ // 获取 skill 信息
14
+ const detailResponse = await apiClient.getSkillDetail(skillId);
15
+
16
+ if (detailResponse.code !== 200) {
17
+ console.error(chalk.red('❌ Skill not found'));
18
+ process.exit(1);
19
+ }
20
+
21
+ const skill = detailResponse.data;
22
+
23
+ console.log(chalk.yellow('\n⚠️ You are about to delete:'));
24
+ console.log(` ${chalk.bold(skill.name)}`);
25
+ console.log(` ${chalk.gray(skill.purpose || 'No description')}`);
26
+ console.log();
27
+
28
+ // 确认删除
29
+ let confirm = options.force;
30
+
31
+ if (!confirm) {
32
+ const answer = await inquirer.prompt([{
33
+ type: 'confirm',
34
+ name: 'confirm',
35
+ message: chalk.red('Are you sure? This action cannot be undone.'),
36
+ default: false
37
+ }]);
38
+ confirm = answer.confirm;
39
+ }
40
+
41
+ if (!confirm) {
42
+ console.log(chalk.gray('Deletion cancelled.\n'));
43
+ return;
44
+ }
45
+
46
+ // 执行删除
47
+ console.log(chalk.blue('\n🗑️ Deleting...\n'));
48
+
49
+ const response = await apiClient.deleteSkill(skillId);
50
+
51
+ if (response.code === 200) {
52
+ console.log(chalk.green('✅ Skill deleted successfully.\n'));
53
+ } else {
54
+ console.error(chalk.red('❌ Deletion failed:'), response.data || 'Unknown error');
55
+ process.exit(1);
56
+ }
57
+ } catch (error) {
58
+ console.error(chalk.red('❌ Delete error:'), error.message);
59
+ process.exit(1);
60
+ }
61
+ }
62
+
63
+ module.exports = remove;