@botskill/cli 1.0.1-alpha.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 +160 -0
- package/package.json +50 -0
- package/scripts/build.js +35 -0
- package/scripts/postinstall.js +25 -0
- package/src/commands/config.js +65 -0
- package/src/commands/get.js +88 -0
- package/src/commands/help.js +34 -0
- package/src/commands/info.js +79 -0
- package/src/commands/init.js +128 -0
- package/src/commands/list.js +84 -0
- package/src/commands/login.js +75 -0
- package/src/commands/logout.js +19 -0
- package/src/commands/publish.js +62 -0
- package/src/commands/push.js +62 -0
- package/src/commands/search.js +61 -0
- package/src/index.js +35 -0
- package/src/lib/auth.js +95 -0
- package/src/lib/constants.js +10 -0
- package/src/lib/uploadSkill.js +91 -0
package/README.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# BotSkill CLI (skm)
|
|
2
|
+
|
|
3
|
+
The official command-line interface for BotSkill, a platform for managing and sharing AI agent skills.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @botskill/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or use without installing:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx @botskill/cli [command]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Commands
|
|
18
|
+
|
|
19
|
+
### init
|
|
20
|
+
Initialize a new skill project (creates skill.config.json):
|
|
21
|
+
```bash
|
|
22
|
+
skm init --name my-skill --description "A new AI skill"
|
|
23
|
+
skm init -y # Use defaults without prompting
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### login
|
|
27
|
+
Login to BotSkill platform:
|
|
28
|
+
```bash
|
|
29
|
+
skm login --token YOUR_TOKEN
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### config
|
|
33
|
+
Manage CLI configuration:
|
|
34
|
+
```bash
|
|
35
|
+
# List all configurations
|
|
36
|
+
skm config --list
|
|
37
|
+
|
|
38
|
+
# Get specific configuration
|
|
39
|
+
skm config --get apiUrl
|
|
40
|
+
|
|
41
|
+
# Set configuration
|
|
42
|
+
skm config --set apiUrl=https://api.botskill.ai
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### get
|
|
46
|
+
Download a skill from BotSkill and extract to directory (default: current directory). Use `name@version` for a specific version, or `name` for latest. API URL from config (optional):
|
|
47
|
+
```bash
|
|
48
|
+
skm get pdf-processing
|
|
49
|
+
skm get pdf-processing@1.0.0
|
|
50
|
+
skm get pdf-processing -o ./my-skills
|
|
51
|
+
skm get pdf-processing --dry-run
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### push / publish
|
|
55
|
+
Upload/push or publish a skill to BotSkill (requires login, publisher or admin role):
|
|
56
|
+
```bash
|
|
57
|
+
# From a directory with skill.config.json
|
|
58
|
+
skm push
|
|
59
|
+
|
|
60
|
+
# Or use publish (alias)
|
|
61
|
+
skm publish
|
|
62
|
+
|
|
63
|
+
# With options
|
|
64
|
+
skm push --name my-skill --description "My AI skill" --category ai
|
|
65
|
+
skm push --dry-run # Validate without uploading
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### list
|
|
69
|
+
List skills from BotSkill (fetches from API):
|
|
70
|
+
```bash
|
|
71
|
+
skm list
|
|
72
|
+
skm list --category ai --limit 10
|
|
73
|
+
skm list --search translator
|
|
74
|
+
skm list --mine # Your skills (requires login)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### search
|
|
78
|
+
Search skills by name or description:
|
|
79
|
+
```bash
|
|
80
|
+
skm search pdf
|
|
81
|
+
skm search translator --category ai
|
|
82
|
+
skm search "data analysis" --limit 10
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### info
|
|
86
|
+
Show skill details (without downloading):
|
|
87
|
+
```bash
|
|
88
|
+
skm info pdf-processing
|
|
89
|
+
skm info pdf-processing@1.0.0
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Configuration
|
|
93
|
+
|
|
94
|
+
安装后会在用户主目录下自动创建 `~/.skm/` 目录及默认配置:
|
|
95
|
+
- **macOS / Linux**: `~/.skm/config.json`
|
|
96
|
+
- **Windows**: `%USERPROFILE%\.skm\config.json`
|
|
97
|
+
|
|
98
|
+
使用 `skm config` 管理配置,`skm config --path` 查看配置文件路径。
|
|
99
|
+
|
|
100
|
+
### 默认配置
|
|
101
|
+
- `apiUrl`: API 地址,优先级:环境变量 `BOTSKILL_API_URL` > 配置文件 > 构建时默认值
|
|
102
|
+
- `token` / `refreshToken`: 登录后自动保存
|
|
103
|
+
|
|
104
|
+
### 环境变量
|
|
105
|
+
- **BOTSKILL_API_URL**:运行时覆盖 API 地址(不修改配置文件)
|
|
106
|
+
|
|
107
|
+
### 发布时指定默认 API 和作者
|
|
108
|
+
```bash
|
|
109
|
+
# 开发/本地默认 localhost
|
|
110
|
+
npm run build
|
|
111
|
+
|
|
112
|
+
# 生产环境
|
|
113
|
+
BOTSKILL_API_URL=https://api.botskill.ai npm run build
|
|
114
|
+
BOTSKILL_API_URL=https://api.botskill.ai npm publish
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Usage Examples
|
|
118
|
+
|
|
119
|
+
### Creating a new skill
|
|
120
|
+
```bash
|
|
121
|
+
# Initialize a new skill project
|
|
122
|
+
skm init --name my-translator --description "AI translation skill"
|
|
123
|
+
|
|
124
|
+
# Edit skill.config.json (add tags, URLs, etc.)
|
|
125
|
+
# Login to BotSkill
|
|
126
|
+
skm login
|
|
127
|
+
|
|
128
|
+
# Push or publish to BotSkill
|
|
129
|
+
skm push
|
|
130
|
+
# or
|
|
131
|
+
skm publish
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Using an existing skill
|
|
135
|
+
```bash
|
|
136
|
+
# Search for skills
|
|
137
|
+
skm list --search translator --category ai
|
|
138
|
+
|
|
139
|
+
# Download a skill (latest version)
|
|
140
|
+
skm get pdf-processing
|
|
141
|
+
skm get pdf-processing@1.0.0 -o ./skills
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Development
|
|
145
|
+
|
|
146
|
+
To run the CLI locally during development:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
cd skm-cli
|
|
150
|
+
npm install
|
|
151
|
+
node src/index.js [command]
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Contributing
|
|
155
|
+
|
|
156
|
+
See our contributing guide for more information on how to contribute to the BotSkill CLI.
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@botskill/cli",
|
|
3
|
+
"version": "1.0.1-alpha.1",
|
|
4
|
+
"description": "CLI tool for BotSkill - AI agent skills platform",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"skm": "src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node src/index.js",
|
|
12
|
+
"dev": "node src/index.js",
|
|
13
|
+
"build": "node scripts/build.js",
|
|
14
|
+
"build:restore": "node scripts/build.js --restore",
|
|
15
|
+
"prepublishOnly": "npm run build",
|
|
16
|
+
"postpublish": "npm run build:restore",
|
|
17
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
18
|
+
"postinstall": "node scripts/postinstall.js"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"src",
|
|
22
|
+
"scripts"
|
|
23
|
+
],
|
|
24
|
+
"keywords": [
|
|
25
|
+
"cli",
|
|
26
|
+
"ai",
|
|
27
|
+
"skills",
|
|
28
|
+
"agents",
|
|
29
|
+
"management",
|
|
30
|
+
"tool"
|
|
31
|
+
],
|
|
32
|
+
"author": "BotSkill Team",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"adm-zip": "^0.5.16",
|
|
36
|
+
"axios": "^1.6.0",
|
|
37
|
+
"commander": "^11.0.0",
|
|
38
|
+
"configstore": "^6.0.0",
|
|
39
|
+
"form-data": "^4.0.5",
|
|
40
|
+
"fs-extra": "^11.1.1",
|
|
41
|
+
"inquirer": "^9.2.0",
|
|
42
|
+
"tar": "^6.2.0"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=16.0.0"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
}
|
|
50
|
+
}
|
package/scripts/build.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 构建脚本:根据环境变量注入默认配置
|
|
4
|
+
* 用法:
|
|
5
|
+
* npm run build # 使用 localhost(开发)
|
|
6
|
+
* BOTSKILL_API_URL=https://api.botskill.ai npm run build # 生产 API
|
|
7
|
+
* npm run build:restore # 发布后恢复占位符,便于继续开发
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const constantsPath = path.join(__dirname, '../src/lib/constants.js');
|
|
15
|
+
|
|
16
|
+
const isRestore = process.argv.includes('--restore');
|
|
17
|
+
|
|
18
|
+
if (isRestore) {
|
|
19
|
+
let content = fs.readFileSync(constantsPath, 'utf8');
|
|
20
|
+
content = content.replace(
|
|
21
|
+
/export const DEFAULT_API_URL = .+;/,
|
|
22
|
+
"export const DEFAULT_API_URL = '__DEFAULT_API_URL__';"
|
|
23
|
+
);
|
|
24
|
+
fs.writeFileSync(constantsPath, content);
|
|
25
|
+
console.log('Build: restored placeholder');
|
|
26
|
+
} else {
|
|
27
|
+
const apiUrl = process.env.BOTSKILL_API_URL || 'http://localhost:3001/api';
|
|
28
|
+
let content = fs.readFileSync(constantsPath, 'utf8');
|
|
29
|
+
content = content.replace(
|
|
30
|
+
/export const DEFAULT_API_URL = .+;/,
|
|
31
|
+
`export const DEFAULT_API_URL = ${JSON.stringify(apiUrl)};`
|
|
32
|
+
);
|
|
33
|
+
fs.writeFileSync(constantsPath, content);
|
|
34
|
+
console.log(`Build: DEFAULT_API_URL = ${apiUrl}`);
|
|
35
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* postinstall: 在用户目录下创建默认配置文件
|
|
4
|
+
* 路径: ~/.skm/config.json
|
|
5
|
+
* 默认 API 来自构建时 BOTSKILL_API_URL,未构建时用 localhost
|
|
6
|
+
*/
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import os from 'os';
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import Configstore from 'configstore';
|
|
11
|
+
import { getDefaultApiUrl } from '../src/lib/constants.js';
|
|
12
|
+
|
|
13
|
+
const CONFIG_PATH = path.join(os.homedir(), '.skm', 'config.json');
|
|
14
|
+
const defaultUrl = getDefaultApiUrl();
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const config = new Configstore('botskill-cli', { apiUrl: defaultUrl }, {
|
|
18
|
+
configPath: CONFIG_PATH,
|
|
19
|
+
});
|
|
20
|
+
if (!fs.existsSync(config.path)) {
|
|
21
|
+
config.set('apiUrl', defaultUrl);
|
|
22
|
+
}
|
|
23
|
+
} catch {
|
|
24
|
+
// 静默失败,首次运行 skm 时 Configstore 会创建
|
|
25
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { getApiUrl, setApiUrl, getToken, getUser, clearAuth, getConfigPath } from '../lib/auth.js';
|
|
3
|
+
import { getDefaultApiUrl } from '../lib/constants.js';
|
|
4
|
+
|
|
5
|
+
const configCommand = new Command('config');
|
|
6
|
+
configCommand
|
|
7
|
+
.description('Manage CLI configuration')
|
|
8
|
+
.option('-g, --get <key>', 'Get configuration value')
|
|
9
|
+
.option('-s, --set <key=value>', 'Set configuration value')
|
|
10
|
+
.option('-l, --list', 'List all configurations')
|
|
11
|
+
.option('-p, --path', 'Show config file path')
|
|
12
|
+
.option('--reset', 'Reset configuration to defaults')
|
|
13
|
+
.action(async (options) => {
|
|
14
|
+
if (options.path) {
|
|
15
|
+
console.log(getConfigPath());
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (options.list) {
|
|
19
|
+
const apiUrl = getApiUrl();
|
|
20
|
+
const token = getToken();
|
|
21
|
+
const user = getUser();
|
|
22
|
+
console.log('Current configuration:');
|
|
23
|
+
console.log(` config: ${getConfigPath()}`);
|
|
24
|
+
console.log(` apiUrl: ${apiUrl}`);
|
|
25
|
+
console.log(` token: ${token ? '***' : '(not set)'}`);
|
|
26
|
+
if (user) {
|
|
27
|
+
console.log(` user: ${user.username || user.email || user.id}`);
|
|
28
|
+
}
|
|
29
|
+
} else if (options.get) {
|
|
30
|
+
const key = options.get;
|
|
31
|
+
if (key === 'apiUrl') {
|
|
32
|
+
console.log(getApiUrl());
|
|
33
|
+
} else if (key === 'token') {
|
|
34
|
+
console.log(getToken() ? '***' : '(not set)');
|
|
35
|
+
} else {
|
|
36
|
+
console.error(`Unknown key: ${key}`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
} else if (options.set) {
|
|
40
|
+
const [key, value] = options.set.split('=');
|
|
41
|
+
if (!key || value === undefined) {
|
|
42
|
+
console.error('Use --set key=value');
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
if (key === 'apiUrl') {
|
|
46
|
+
setApiUrl(value.trim());
|
|
47
|
+
console.log(`apiUrl set to ${value}`);
|
|
48
|
+
} else {
|
|
49
|
+
console.error(`Unknown key: ${key}`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
} else if (options.reset) {
|
|
53
|
+
clearAuth();
|
|
54
|
+
setApiUrl(getDefaultApiUrl());
|
|
55
|
+
console.log('Configuration reset to defaults.');
|
|
56
|
+
} else {
|
|
57
|
+
console.log('Usage:');
|
|
58
|
+
console.log(' skm config --list List configuration');
|
|
59
|
+
console.log(' skm config --get apiUrl Get apiUrl');
|
|
60
|
+
console.log(' skm config --set apiUrl=URL Set apiUrl');
|
|
61
|
+
console.log(' skm config --reset Reset to defaults');
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export { configCommand };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import AdmZip from 'adm-zip';
|
|
4
|
+
import { createApiClient } from '../lib/auth.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parse specifier: name@version or name
|
|
8
|
+
* Returns { name, version } for API
|
|
9
|
+
*/
|
|
10
|
+
function parseSpecifier(spec) {
|
|
11
|
+
const s = spec.trim();
|
|
12
|
+
const atIdx = s.lastIndexOf('@');
|
|
13
|
+
if (atIdx < 0) return { name: s, version: undefined };
|
|
14
|
+
return {
|
|
15
|
+
name: s.slice(0, atIdx).trim(),
|
|
16
|
+
version: s.slice(atIdx + 1).trim() || undefined,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const getCommand = new Command('get');
|
|
21
|
+
getCommand
|
|
22
|
+
.description('Download a skill from BotSkill and extract to directory')
|
|
23
|
+
.argument('<specifier>', 'Skill name or name@version (e.g. pdf-parser or pdf-parser@1.0.0)')
|
|
24
|
+
.option('-o, --output <dir>', 'Output directory (default: current directory)')
|
|
25
|
+
.option('--dry-run', 'Show what would be downloaded without actually downloading')
|
|
26
|
+
.action(async (specifier, options) => {
|
|
27
|
+
const { name, version } = parseSpecifier(specifier);
|
|
28
|
+
const outputDir = path.resolve(options.output || process.cwd());
|
|
29
|
+
|
|
30
|
+
if (!name) {
|
|
31
|
+
console.error('Error: skill name is required');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (options.dryRun) {
|
|
36
|
+
console.log('[DRY RUN] Would download skill:', name);
|
|
37
|
+
console.log('[DRY RUN] Version:', version || 'latest');
|
|
38
|
+
console.log('[DRY RUN] Output:', outputDir);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const api = createApiClient();
|
|
44
|
+
|
|
45
|
+
const fullSpec = version ? `${name}@${version}` : name;
|
|
46
|
+
console.log(`Downloading skill: ${fullSpec}`);
|
|
47
|
+
console.log(`Version: ${version || 'latest'}`);
|
|
48
|
+
console.log(`Output: ${outputDir}`);
|
|
49
|
+
|
|
50
|
+
const resolveRes = await api.get(`/skills/by-name/${encodeURIComponent(fullSpec)}`);
|
|
51
|
+
const skill = resolveRes.data?.skill ?? resolveRes.data;
|
|
52
|
+
if (!skill?._id) {
|
|
53
|
+
console.error('Download failed: Skill not found');
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const versionParam = version ? `?version=${encodeURIComponent(version)}` : '';
|
|
58
|
+
const url = `/skills/${encodeURIComponent(skill._id)}/download${versionParam}`;
|
|
59
|
+
const res = await api.get(url, { responseType: 'arraybuffer' });
|
|
60
|
+
const buffer = Buffer.from(res.data);
|
|
61
|
+
|
|
62
|
+
const zip = new AdmZip(buffer);
|
|
63
|
+
zip.extractAllTo(outputDir, true);
|
|
64
|
+
|
|
65
|
+
const entries = zip.getEntries();
|
|
66
|
+
const skillDir = entries.find(e => e.isDirectory)?.entryName || entries[0]?.entryName?.split('/')[0] || 'skill';
|
|
67
|
+
const targetPath = path.join(outputDir, skillDir);
|
|
68
|
+
|
|
69
|
+
console.log(`\nSkill downloaded successfully!`);
|
|
70
|
+
console.log(`Location: ${targetPath}`);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
let msg = err.message;
|
|
73
|
+
if (err.response?.data) {
|
|
74
|
+
const raw = err.response.data;
|
|
75
|
+
const str = Buffer.isBuffer(raw) ? raw.toString() : (typeof raw === 'string' ? raw : JSON.stringify(raw));
|
|
76
|
+
try {
|
|
77
|
+
const obj = JSON.parse(str);
|
|
78
|
+
msg = obj.error || obj.message || msg;
|
|
79
|
+
} catch (_) {
|
|
80
|
+
msg = str || msg;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
console.error('Download failed:', msg);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export { getCommand };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
|
|
3
|
+
const helpCommand = new Command('help');
|
|
4
|
+
helpCommand
|
|
5
|
+
.description('Show help information')
|
|
6
|
+
.action(async () => {
|
|
7
|
+
console.log('BotSkill Manager (skm) - CLI Tool');
|
|
8
|
+
console.log('');
|
|
9
|
+
console.log('Usage: skm [options] [command]');
|
|
10
|
+
console.log('');
|
|
11
|
+
console.log('Options:');
|
|
12
|
+
console.log(' -V, --version output the version number');
|
|
13
|
+
console.log(' -h, --help display help for command');
|
|
14
|
+
console.log('');
|
|
15
|
+
console.log('Commands:');
|
|
16
|
+
console.log(' init [options] Initialize a new skill project');
|
|
17
|
+
console.log(' login [options] Login to BotSkill platform');
|
|
18
|
+
console.log(' config [options] Manage CLI configuration');
|
|
19
|
+
console.log(' get <skill-id> Download a skill from BotSkill');
|
|
20
|
+
console.log(' push [options] Upload/push a skill to BotSkill');
|
|
21
|
+
console.log(' list [options] List skills from BotSkill');
|
|
22
|
+
console.log(' help [command] display help for command');
|
|
23
|
+
console.log('');
|
|
24
|
+
console.log('Examples:');
|
|
25
|
+
console.log(' skm init --name my-skill');
|
|
26
|
+
console.log(' skm login --token abc123');
|
|
27
|
+
console.log(' skm list --category ai');
|
|
28
|
+
console.log(' skm get gpt-translator');
|
|
29
|
+
console.log(' skm push --public');
|
|
30
|
+
console.log('');
|
|
31
|
+
console.log('For detailed help on any command, use: skm [command] --help');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export { helpCommand };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { createApiClient } from '../lib/auth.js';
|
|
3
|
+
|
|
4
|
+
function parseSpecifier(spec) {
|
|
5
|
+
const s = spec.trim();
|
|
6
|
+
const atIdx = s.lastIndexOf('@');
|
|
7
|
+
if (atIdx < 0) return { name: s, version: undefined };
|
|
8
|
+
return {
|
|
9
|
+
name: s.slice(0, atIdx).trim(),
|
|
10
|
+
version: s.slice(atIdx + 1).trim() || undefined,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const infoCommand = new Command('info');
|
|
15
|
+
infoCommand
|
|
16
|
+
.description('Show skill details from BotSkill')
|
|
17
|
+
.argument('<specifier>', 'Skill name or name@version')
|
|
18
|
+
.action(async (specifier) => {
|
|
19
|
+
const { name, version } = parseSpecifier(specifier);
|
|
20
|
+
|
|
21
|
+
if (!name) {
|
|
22
|
+
console.error('Error: skill name is required');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const api = createApiClient();
|
|
28
|
+
const fullSpec = version ? `${name}@${version}` : name;
|
|
29
|
+
|
|
30
|
+
const resolveRes = await api.get(`/skills/by-name/${encodeURIComponent(fullSpec)}`);
|
|
31
|
+
const skill = resolveRes.data?.skill ?? resolveRes.data;
|
|
32
|
+
if (!skill?._id) {
|
|
33
|
+
console.error('Skill not found');
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log('\n' + '─'.repeat(50));
|
|
38
|
+
console.log(` ${skill.name}`);
|
|
39
|
+
console.log('─'.repeat(50));
|
|
40
|
+
console.log(` Description: ${skill.description || '—'}`);
|
|
41
|
+
const author = skill.author?.username || skill.author?.fullName || '—';
|
|
42
|
+
console.log(` Author: ${author}`);
|
|
43
|
+
console.log(` Category: ${skill.category || '—'}`);
|
|
44
|
+
console.log(` Downloads: ${(skill.downloads ?? 0).toLocaleString()}`);
|
|
45
|
+
console.log(` License: ${skill.license || 'MIT'}`);
|
|
46
|
+
if (skill.tags?.length) {
|
|
47
|
+
console.log(` Tags: ${skill.tags.join(', ')}`);
|
|
48
|
+
}
|
|
49
|
+
if (skill.repositoryUrl) {
|
|
50
|
+
console.log(` Repository: ${skill.repositoryUrl}`);
|
|
51
|
+
}
|
|
52
|
+
if (skill.documentationUrl) {
|
|
53
|
+
console.log(` Docs: ${skill.documentationUrl}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const versions = skill.versions || [];
|
|
57
|
+
const versionList = versions.length > 0 ? versions : (skill.version ? [{ version: skill.version, description: skill.description }] : []);
|
|
58
|
+
if (versionList.length > 0) {
|
|
59
|
+
console.log('\n Versions:');
|
|
60
|
+
versionList.forEach((v) => {
|
|
61
|
+
const date = v.createdAt ? new Date(v.createdAt).toLocaleDateString() : '';
|
|
62
|
+
console.log(` - ${v.version}${date ? ` (${date})` : ''}`);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log('\n Use "skm get name" or "skm get name@version" to download.');
|
|
67
|
+
console.log('');
|
|
68
|
+
} catch (err) {
|
|
69
|
+
let msg = err.message;
|
|
70
|
+
if (err.response?.data) {
|
|
71
|
+
const d = err.response.data;
|
|
72
|
+
msg = d.error || d.message || msg;
|
|
73
|
+
}
|
|
74
|
+
console.error('Error:', msg);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
export { infoCommand };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import inquirer from 'inquirer';
|
|
5
|
+
|
|
6
|
+
const initCommand = new Command('init');
|
|
7
|
+
initCommand
|
|
8
|
+
.description('Initialize a new skill project')
|
|
9
|
+
.option('-n, --name <name>', 'Project/skill name')
|
|
10
|
+
.option('-d, --description <description>', 'Skill description')
|
|
11
|
+
.option('-c, --category <category>', 'Category: ai, data, web, devops, security, tools')
|
|
12
|
+
.option('-y, --yes', 'Use defaults without prompting')
|
|
13
|
+
.action(async (options) => {
|
|
14
|
+
const cwd = process.cwd();
|
|
15
|
+
const configPath = path.join(cwd, 'skill.config.json');
|
|
16
|
+
|
|
17
|
+
if (await fs.pathExists(configPath)) {
|
|
18
|
+
console.error('skill.config.json already exists in this directory.');
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const validCategories = ['ai', 'data', 'web', 'devops', 'security', 'tools'];
|
|
23
|
+
let answers = {};
|
|
24
|
+
|
|
25
|
+
if (options.yes) {
|
|
26
|
+
answers = {
|
|
27
|
+
name: options.name || 'my-skill',
|
|
28
|
+
description: options.description || 'A new AI skill',
|
|
29
|
+
category: options.category || 'tools',
|
|
30
|
+
version: '1.0.0',
|
|
31
|
+
license: 'MIT',
|
|
32
|
+
};
|
|
33
|
+
} else {
|
|
34
|
+
answers = await inquirer.prompt([
|
|
35
|
+
{
|
|
36
|
+
type: 'input',
|
|
37
|
+
name: 'name',
|
|
38
|
+
message: 'Skill name:',
|
|
39
|
+
default: options.name || 'my-skill',
|
|
40
|
+
validate: (v) => (v && v.length >= 2 ? true : 'Name must be at least 2 characters'),
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
type: 'input',
|
|
44
|
+
name: 'description',
|
|
45
|
+
message: 'Description:',
|
|
46
|
+
default: options.description || 'A new AI skill',
|
|
47
|
+
validate: (v) => (v ? true : 'Description is required'),
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
type: 'list',
|
|
51
|
+
name: 'category',
|
|
52
|
+
message: 'Category:',
|
|
53
|
+
choices: validCategories,
|
|
54
|
+
default: options.category && validCategories.includes(options.category) ? options.category : 'tools',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
type: 'input',
|
|
58
|
+
name: 'version',
|
|
59
|
+
message: 'Version:',
|
|
60
|
+
default: '1.0.0',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
type: 'input',
|
|
64
|
+
name: 'license',
|
|
65
|
+
message: 'License:',
|
|
66
|
+
default: 'MIT',
|
|
67
|
+
},
|
|
68
|
+
]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const config = {
|
|
72
|
+
name: answers.name,
|
|
73
|
+
description: answers.description,
|
|
74
|
+
version: answers.version,
|
|
75
|
+
category: answers.category,
|
|
76
|
+
license: answers.license,
|
|
77
|
+
tags: [],
|
|
78
|
+
repositoryUrl: '',
|
|
79
|
+
documentationUrl: '',
|
|
80
|
+
demoUrl: '',
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
await fs.writeJson(configPath, config, { spaces: 2 });
|
|
84
|
+
|
|
85
|
+
// Agent Skills spec: https://agentskills.io/specification
|
|
86
|
+
// name: required, 1-64 chars, lowercase + hyphens
|
|
87
|
+
// description: required, max 1024 chars
|
|
88
|
+
// metadata.version, metadata.author: optional
|
|
89
|
+
const rawName = String(answers.name || 'my-skill').trim();
|
|
90
|
+
const skillName = rawName.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '').replace(/-+/g, '-').replace(/^-|-$/g, '') || 'my-skill';
|
|
91
|
+
const skillMd = `---
|
|
92
|
+
name: ${skillName}
|
|
93
|
+
description: ${answers.description}
|
|
94
|
+
license: ${answers.license}
|
|
95
|
+
metadata:
|
|
96
|
+
author: ${skillName}
|
|
97
|
+
version: "${answers.version}"
|
|
98
|
+
# Platform-specific (optional)
|
|
99
|
+
category: ${answers.category}
|
|
100
|
+
tags: []
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
# ${answers.name}
|
|
104
|
+
|
|
105
|
+
${answers.description}
|
|
106
|
+
|
|
107
|
+
## Usage
|
|
108
|
+
|
|
109
|
+
Add your usage documentation here. The Markdown body contains skill instructions for agents.
|
|
110
|
+
|
|
111
|
+
## Installation
|
|
112
|
+
|
|
113
|
+
\`\`\`bash
|
|
114
|
+
# Add installation instructions
|
|
115
|
+
\`\`\`
|
|
116
|
+
`;
|
|
117
|
+
|
|
118
|
+
const skillMdPath = path.join(cwd, 'SKILL.md');
|
|
119
|
+
await fs.writeFile(skillMdPath, skillMd, 'utf-8');
|
|
120
|
+
|
|
121
|
+
console.log('Created skill.config.json and SKILL.md');
|
|
122
|
+
console.log('\nNext steps:');
|
|
123
|
+
console.log('1. Edit SKILL.md to add documentation (the content below the frontmatter)');
|
|
124
|
+
console.log('2. Run "skm login" to authenticate');
|
|
125
|
+
console.log('3. Run "skm push" or "skm publish" to upload your skill');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
export { initCommand };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { createApiClient } from '../lib/auth.js';
|
|
3
|
+
import { isLoggedIn } from '../lib/auth.js';
|
|
4
|
+
|
|
5
|
+
function formatSkillDisplay(skill) {
|
|
6
|
+
const name = skill.name || '?';
|
|
7
|
+
const version = skill.version || (skill.versions?.[0]?.version) || '—';
|
|
8
|
+
const downloads = skill.downloads ?? 0;
|
|
9
|
+
const category = skill.category || '—';
|
|
10
|
+
const status = skill.status || '—';
|
|
11
|
+
return { name, version, downloads, category, status };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const listCommand = new Command('list');
|
|
15
|
+
listCommand
|
|
16
|
+
.alias('ls')
|
|
17
|
+
.description('List skills from BotSkill')
|
|
18
|
+
.option('-c, --category <category>', 'Filter by category (ai, data, web, devops, security, tools)')
|
|
19
|
+
.option('-s, --search <query>', 'Search skills by name or description')
|
|
20
|
+
.option('-m, --mine', 'Show only your skills (requires login)')
|
|
21
|
+
.option('-l, --limit <number>', 'Maximum number of results (default: 20)', '20')
|
|
22
|
+
.option('-p, --page <number>', 'Page number for pagination (default: 1)', '1')
|
|
23
|
+
.action(async (options) => {
|
|
24
|
+
const api = createApiClient();
|
|
25
|
+
const limit = parseInt(options.limit, 10) || 20;
|
|
26
|
+
const page = parseInt(options.page, 10) || 1;
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
let res;
|
|
30
|
+
if (options.mine) {
|
|
31
|
+
if (!isLoggedIn()) {
|
|
32
|
+
console.error('Error: --mine requires login. Run "skm login" first.');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
const params = { page, limit };
|
|
36
|
+
if (options.category) params.category = options.category;
|
|
37
|
+
if (options.search) params.q = options.search;
|
|
38
|
+
res = await api.get('/skills/my', { params });
|
|
39
|
+
} else {
|
|
40
|
+
const params = { page, limit };
|
|
41
|
+
if (options.category) params.category = options.category;
|
|
42
|
+
if (options.search) params.q = options.search;
|
|
43
|
+
if (params.q || params.category) {
|
|
44
|
+
res = await api.get('/skills/search', { params });
|
|
45
|
+
} else {
|
|
46
|
+
res = await api.get('/skills', { params });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const skills = res.data?.skills ?? res.data ?? [];
|
|
51
|
+
const pagination = res.data?.pagination ?? {};
|
|
52
|
+
|
|
53
|
+
if (skills.length === 0) {
|
|
54
|
+
console.log('No skills found.');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log(`\nFound ${pagination.totalSkills ?? skills.length} skill(s):`);
|
|
59
|
+
console.log('─'.repeat(60));
|
|
60
|
+
skills.forEach((skill) => {
|
|
61
|
+
const { name, version, downloads, category, status } = formatSkillDisplay(skill);
|
|
62
|
+
const statusStr = options.mine ? ` | ${status}` : '';
|
|
63
|
+
console.log(` ${name}`);
|
|
64
|
+
console.log(` Version: ${version} | Downloads: ${downloads} | Category: ${category}${statusStr}`);
|
|
65
|
+
});
|
|
66
|
+
if (pagination.totalPages > 1) {
|
|
67
|
+
console.log(`\nPage ${pagination.currentPage}/${pagination.totalPages}`);
|
|
68
|
+
}
|
|
69
|
+
console.log('\nUse "skm get name" or "skm get name@version" to download.');
|
|
70
|
+
} catch (err) {
|
|
71
|
+
let msg = err.message;
|
|
72
|
+
if (err.response?.data) {
|
|
73
|
+
const d = err.response.data;
|
|
74
|
+
msg = d.error || d.message || msg;
|
|
75
|
+
}
|
|
76
|
+
if (err.response?.status === 401 && options.mine) {
|
|
77
|
+
msg = 'Login required. Run "skm login" first.';
|
|
78
|
+
}
|
|
79
|
+
console.error('Error:', msg);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
export { listCommand };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
import { getApiUrl, setAuth, setApiUrl } from '../lib/auth.js';
|
|
5
|
+
|
|
6
|
+
const loginCommand = new Command('login');
|
|
7
|
+
loginCommand
|
|
8
|
+
.description('Login to BotSkill platform')
|
|
9
|
+
.option('-u, --username <username>', 'Username')
|
|
10
|
+
.option('-e, --email <email>', 'Email address')
|
|
11
|
+
.option('-p, --password <password>', 'Password')
|
|
12
|
+
.option('-t, --token <token>', 'Use access token directly (from web)')
|
|
13
|
+
.option('--api-url <url>', 'API base URL')
|
|
14
|
+
.action(async (options) => {
|
|
15
|
+
const apiUrl = options.apiUrl || getApiUrl();
|
|
16
|
+
if (options.apiUrl) {
|
|
17
|
+
setApiUrl(options.apiUrl);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (options.token) {
|
|
21
|
+
setAuth({ token: options.token });
|
|
22
|
+
console.log('Token saved. Logged in successfully.');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let emailOrUsername = options.email || options.username;
|
|
27
|
+
let password = options.password;
|
|
28
|
+
|
|
29
|
+
if (!emailOrUsername || !password) {
|
|
30
|
+
const answers = await inquirer.prompt([
|
|
31
|
+
{
|
|
32
|
+
type: 'input',
|
|
33
|
+
name: 'emailOrUsername',
|
|
34
|
+
message: 'Email or Username:',
|
|
35
|
+
default: emailOrUsername,
|
|
36
|
+
validate: (v) => (v?.trim() ? true : 'Required'),
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
type: 'password',
|
|
40
|
+
name: 'password',
|
|
41
|
+
message: 'Password:',
|
|
42
|
+
mask: '*',
|
|
43
|
+
validate: (v) => (v ? true : 'Required'),
|
|
44
|
+
},
|
|
45
|
+
]);
|
|
46
|
+
emailOrUsername = answers.emailOrUsername?.trim();
|
|
47
|
+
password = answers.password;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log('Logging in to BotSkill...');
|
|
51
|
+
try {
|
|
52
|
+
const res = await axios.post(`${apiUrl}/auth/login`, {
|
|
53
|
+
email: emailOrUsername,
|
|
54
|
+
password,
|
|
55
|
+
});
|
|
56
|
+
const data = res.data?.data || res.data;
|
|
57
|
+
const accessToken = data.accessToken || data.token;
|
|
58
|
+
const refreshToken = data.refreshToken;
|
|
59
|
+
const user = data.user;
|
|
60
|
+
|
|
61
|
+
if (!accessToken) {
|
|
62
|
+
console.error('Login failed: No token received');
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
setAuth({ token: accessToken, refreshToken, user });
|
|
67
|
+
console.log(`Logged in as ${user?.username || user?.email || 'user'}`);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
const msg = err.response?.data?.error || err.message || 'Login failed';
|
|
70
|
+
console.error('Login failed:', msg);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export { loginCommand };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { clearAuth, getApiUrl, getRefreshToken } from '../lib/auth.js';
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
|
|
5
|
+
const logoutCommand = new Command('logout');
|
|
6
|
+
logoutCommand
|
|
7
|
+
.description('Logout from BotSkill')
|
|
8
|
+
.action(async () => {
|
|
9
|
+
const refreshToken = getRefreshToken();
|
|
10
|
+
if (refreshToken) {
|
|
11
|
+
try {
|
|
12
|
+
await axios.post(`${getApiUrl()}/auth/logout`, { refreshToken });
|
|
13
|
+
} catch (_) {}
|
|
14
|
+
}
|
|
15
|
+
clearAuth();
|
|
16
|
+
console.log('Logged out successfully.');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export { logoutCommand };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import { getToken } from '../lib/auth.js';
|
|
5
|
+
import { uploadSkillFile, findUploadFile } from '../lib/uploadSkill.js';
|
|
6
|
+
|
|
7
|
+
const publishCommand = new Command('publish');
|
|
8
|
+
publishCommand
|
|
9
|
+
.description('Publish a skill to BotSkill (alias for push)')
|
|
10
|
+
.option('-f, --file <path>', 'Path to SKILL.md, .zip, or .tar.gz')
|
|
11
|
+
.option('--dry-run', 'Validate without uploading')
|
|
12
|
+
.option('--api-url <url>', 'API base URL')
|
|
13
|
+
.action(async (options) => {
|
|
14
|
+
if (!getToken()) {
|
|
15
|
+
console.error('Not logged in. Run: skm login');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let filePath = options.file;
|
|
20
|
+
if (!filePath) {
|
|
21
|
+
filePath = await findUploadFile();
|
|
22
|
+
if (!filePath) {
|
|
23
|
+
console.error('No skill file found. Create SKILL.md or a .zip/.tar.gz package, or use --file <path>');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
} else {
|
|
27
|
+
if (!await fs.pathExists(filePath)) {
|
|
28
|
+
console.error('File not found:', filePath);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (options.dryRun) {
|
|
34
|
+
console.log('[DRY RUN] Would publish:', path.resolve(filePath));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log(`Publishing skill from ${path.basename(filePath)}...`);
|
|
39
|
+
try {
|
|
40
|
+
const skill = await uploadSkillFile(filePath);
|
|
41
|
+
console.log('Skill published successfully!');
|
|
42
|
+
console.log(`Name: ${skill?.name}`);
|
|
43
|
+
console.log(`Version: ${skill?.version || (skill?.versions?.[0]?.version)}`);
|
|
44
|
+
console.log(`Status: ${skill?.status || 'pending_review'}`);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
if (err.message === 'NOT_LOGGED_IN') {
|
|
47
|
+
console.error('Not logged in. Run: skm login');
|
|
48
|
+
} else if (err.message === 'FILE_NOT_FOUND') {
|
|
49
|
+
console.error('File not found');
|
|
50
|
+
} else {
|
|
51
|
+
const msg = err.response?.data?.error || err.response?.data?.details?.[0] || err.message || 'Publish failed';
|
|
52
|
+
if (err.response?.status === 401) {
|
|
53
|
+
console.error('Token expired or invalid. Run: skm login');
|
|
54
|
+
} else {
|
|
55
|
+
console.error('Publish failed:', msg);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export { publishCommand };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import { getToken } from '../lib/auth.js';
|
|
5
|
+
import { uploadSkillFile, findUploadFile, validCategories } from '../lib/uploadSkill.js';
|
|
6
|
+
|
|
7
|
+
const pushCommand = new Command('push');
|
|
8
|
+
pushCommand
|
|
9
|
+
.description('Upload/push a skill to BotSkill (SKILL.md, .zip, or .tar.gz)')
|
|
10
|
+
.option('-f, --file <path>', 'Path to SKILL.md, .zip, or .tar.gz')
|
|
11
|
+
.option('--dry-run', 'Validate without uploading')
|
|
12
|
+
.option('--api-url <url>', 'API base URL')
|
|
13
|
+
.action(async (options) => {
|
|
14
|
+
if (!getToken()) {
|
|
15
|
+
console.error('Not logged in. Run: skm login');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let filePath = options.file;
|
|
20
|
+
if (!filePath) {
|
|
21
|
+
filePath = await findUploadFile();
|
|
22
|
+
if (!filePath) {
|
|
23
|
+
console.error('No skill file found. Create SKILL.md or a .zip/.tar.gz package, or use --file <path>');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
} else {
|
|
27
|
+
if (!await fs.pathExists(filePath)) {
|
|
28
|
+
console.error('File not found:', filePath);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (options.dryRun) {
|
|
34
|
+
console.log('[DRY RUN] Would upload:', path.resolve(filePath));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log(`Pushing skill from ${path.basename(filePath)}...`);
|
|
39
|
+
try {
|
|
40
|
+
const skill = await uploadSkillFile(filePath);
|
|
41
|
+
console.log('Skill uploaded successfully!');
|
|
42
|
+
console.log(`Name: ${skill?.name}`);
|
|
43
|
+
console.log(`Version: ${skill?.version || (skill?.versions?.[0]?.version)}`);
|
|
44
|
+
console.log(`Status: ${skill?.status || 'pending_review'}`);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
if (err.message === 'NOT_LOGGED_IN') {
|
|
47
|
+
console.error('Not logged in. Run: skm login');
|
|
48
|
+
} else if (err.message === 'FILE_NOT_FOUND') {
|
|
49
|
+
console.error('File not found');
|
|
50
|
+
} else {
|
|
51
|
+
const msg = err.response?.data?.error || err.response?.data?.details?.[0] || err.message || 'Upload failed';
|
|
52
|
+
if (err.response?.status === 401) {
|
|
53
|
+
console.error('Token expired or invalid. Run: skm login');
|
|
54
|
+
} else {
|
|
55
|
+
console.error('Upload failed:', msg);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export { pushCommand };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { createApiClient } from '../lib/auth.js';
|
|
3
|
+
|
|
4
|
+
function formatSkillDisplay(skill) {
|
|
5
|
+
const author = skill.author?.username || skill.author?.fullName || '?';
|
|
6
|
+
const name = skill.name || '?';
|
|
7
|
+
const displayName = `@${author}/${name}`;
|
|
8
|
+
const version = skill.version || (skill.versions?.[0]?.version) || '—';
|
|
9
|
+
const downloads = skill.downloads ?? 0;
|
|
10
|
+
const category = skill.category || '—';
|
|
11
|
+
return { displayName, version, downloads, category };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const searchCommand = new Command('search');
|
|
15
|
+
searchCommand
|
|
16
|
+
.description('Search skills from BotSkill')
|
|
17
|
+
.argument('<query>', 'Search query (name or description)')
|
|
18
|
+
.option('-c, --category <category>', 'Filter by category (ai, data, web, devops, security, tools)')
|
|
19
|
+
.option('-l, --limit <number>', 'Maximum number of results (default: 20)', '20')
|
|
20
|
+
.option('-p, --page <number>', 'Page number for pagination (default: 1)', '1')
|
|
21
|
+
.action(async (query, options) => {
|
|
22
|
+
const api = createApiClient();
|
|
23
|
+
const limit = parseInt(options.limit, 10) || 20;
|
|
24
|
+
const page = parseInt(options.page, 10) || 1;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const params = { q: query, page, limit };
|
|
28
|
+
if (options.category) params.category = options.category;
|
|
29
|
+
|
|
30
|
+
const res = await api.get('/skills/search', { params });
|
|
31
|
+
const skills = res.data?.skills ?? res.data ?? [];
|
|
32
|
+
const pagination = res.data?.pagination ?? {};
|
|
33
|
+
|
|
34
|
+
if (skills.length === 0) {
|
|
35
|
+
console.log(`No skills found for "${query}".`);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log(`\nFound ${pagination.totalSkills ?? skills.length} skill(s) for "${query}":`);
|
|
40
|
+
console.log('─'.repeat(60));
|
|
41
|
+
skills.forEach((skill) => {
|
|
42
|
+
const { displayName, version, downloads, category } = formatSkillDisplay(skill);
|
|
43
|
+
console.log(` ${displayName}`);
|
|
44
|
+
console.log(` Version: ${version} | Downloads: ${downloads} | Category: ${category}`);
|
|
45
|
+
});
|
|
46
|
+
if (pagination.totalPages > 1) {
|
|
47
|
+
console.log(`\nPage ${pagination.currentPage}/${pagination.totalPages}`);
|
|
48
|
+
}
|
|
49
|
+
console.log('\nUse "skm get @author/name" or "skm get @author/name@version" to download.');
|
|
50
|
+
} catch (err) {
|
|
51
|
+
let msg = err.message;
|
|
52
|
+
if (err.response?.data) {
|
|
53
|
+
const d = err.response.data;
|
|
54
|
+
msg = d.error || d.message || msg;
|
|
55
|
+
}
|
|
56
|
+
console.error('Error:', msg);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export { searchCommand };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { initCommand } from './commands/init.js';
|
|
5
|
+
import { loginCommand } from './commands/login.js';
|
|
6
|
+
import { logoutCommand } from './commands/logout.js';
|
|
7
|
+
import { configCommand } from './commands/config.js';
|
|
8
|
+
import { getCommand } from './commands/get.js';
|
|
9
|
+
import { pushCommand } from './commands/push.js';
|
|
10
|
+
import { publishCommand } from './commands/publish.js';
|
|
11
|
+
import { listCommand } from './commands/list.js';
|
|
12
|
+
import { searchCommand } from './commands/search.js';
|
|
13
|
+
import { infoCommand } from './commands/info.js';
|
|
14
|
+
import { helpCommand } from './commands/help.js';
|
|
15
|
+
|
|
16
|
+
const program = new Command();
|
|
17
|
+
|
|
18
|
+
program
|
|
19
|
+
.name('skm')
|
|
20
|
+
.description('CLI tool for managing BotSkill - a platform for AI agent skills')
|
|
21
|
+
.version('1.0.0');
|
|
22
|
+
|
|
23
|
+
program.addCommand(initCommand);
|
|
24
|
+
program.addCommand(loginCommand);
|
|
25
|
+
program.addCommand(logoutCommand);
|
|
26
|
+
program.addCommand(configCommand);
|
|
27
|
+
program.addCommand(getCommand);
|
|
28
|
+
program.addCommand(pushCommand);
|
|
29
|
+
program.addCommand(publishCommand);
|
|
30
|
+
program.addCommand(listCommand);
|
|
31
|
+
program.addCommand(searchCommand);
|
|
32
|
+
program.addCommand(infoCommand);
|
|
33
|
+
program.addCommand(helpCommand);
|
|
34
|
+
|
|
35
|
+
program.parse();
|
package/src/lib/auth.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import Configstore from 'configstore';
|
|
4
|
+
import axios from 'axios';
|
|
5
|
+
import { getDefaultApiUrl } from './constants.js';
|
|
6
|
+
|
|
7
|
+
const CONFIG_PATH = path.join(os.homedir(), '.skm', 'config.json');
|
|
8
|
+
const defaultUrl = getDefaultApiUrl();
|
|
9
|
+
|
|
10
|
+
const config = new Configstore('botskill-cli', { apiUrl: defaultUrl }, {
|
|
11
|
+
configPath: CONFIG_PATH,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const getConfigPath = () => config.path;
|
|
15
|
+
|
|
16
|
+
/** 优先级: 环境变量 BOTSKILL_API_URL > 配置文件 > 构建时默认值 */
|
|
17
|
+
export const getApiUrl = () =>
|
|
18
|
+
process.env.BOTSKILL_API_URL || config.get('apiUrl') || getDefaultApiUrl();
|
|
19
|
+
|
|
20
|
+
export const setApiUrl = (url) => config.set('apiUrl', url);
|
|
21
|
+
|
|
22
|
+
export const getToken = () => config.get('token');
|
|
23
|
+
|
|
24
|
+
export const getRefreshToken = () => config.get('refreshToken');
|
|
25
|
+
|
|
26
|
+
export const getUser = () => config.get('user');
|
|
27
|
+
|
|
28
|
+
export const setAuth = (data) => {
|
|
29
|
+
if (data.token || data.accessToken) {
|
|
30
|
+
config.set('token', data.token || data.accessToken);
|
|
31
|
+
}
|
|
32
|
+
if (data.refreshToken) {
|
|
33
|
+
config.set('refreshToken', data.refreshToken);
|
|
34
|
+
}
|
|
35
|
+
if (data.user) {
|
|
36
|
+
config.set('user', data.user);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const clearAuth = () => {
|
|
41
|
+
config.delete('token');
|
|
42
|
+
config.delete('refreshToken');
|
|
43
|
+
config.delete('user');
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const isLoggedIn = () => !!config.get('token');
|
|
47
|
+
|
|
48
|
+
export const createApiClient = () => {
|
|
49
|
+
const baseURL = getApiUrl();
|
|
50
|
+
const client = axios.create({
|
|
51
|
+
baseURL,
|
|
52
|
+
timeout: 15000,
|
|
53
|
+
headers: { 'Content-Type': 'application/json' },
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
client.interceptors.request.use((cfg) => {
|
|
57
|
+
const token = getToken();
|
|
58
|
+
if (token) {
|
|
59
|
+
cfg.headers.Authorization = `Bearer ${token}`;
|
|
60
|
+
}
|
|
61
|
+
return cfg;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
client.interceptors.response.use(
|
|
65
|
+
(res) => res,
|
|
66
|
+
async (err) => {
|
|
67
|
+
if (err.response?.status !== 401) return Promise.reject(err);
|
|
68
|
+
const req = err.config;
|
|
69
|
+
if (req._retry) return Promise.reject(err);
|
|
70
|
+
if (req.url?.includes('/auth/login') || req.url?.includes('/auth/refresh')) {
|
|
71
|
+
return Promise.reject(err);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const refreshToken = getRefreshToken();
|
|
75
|
+
if (!refreshToken) return Promise.reject(err);
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const res = await axios.post(`${baseURL}/auth/refresh`, { refreshToken });
|
|
79
|
+
const data = res.data?.data || res.data;
|
|
80
|
+
const newToken = data.accessToken || data.token;
|
|
81
|
+
const newRefresh = data.refreshToken;
|
|
82
|
+
if (newToken) {
|
|
83
|
+
config.set('token', newToken);
|
|
84
|
+
if (newRefresh) config.set('refreshToken', newRefresh);
|
|
85
|
+
req._retry = true;
|
|
86
|
+
req.headers.Authorization = `Bearer ${newToken}`;
|
|
87
|
+
return client(req);
|
|
88
|
+
}
|
|
89
|
+
} catch (_) {}
|
|
90
|
+
return Promise.reject(err);
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return client;
|
|
95
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 默认 API 地址,可通过 build 时环境变量 BOTSKILL_API_URL 注入
|
|
3
|
+
* 发布生产: BOTSKILL_API_URL=https://api.botskill.ai npm run build
|
|
4
|
+
* 开发/本地: 保持 __DEFAULT_API_URL__ 时使用 localhost
|
|
5
|
+
*/
|
|
6
|
+
export const DEFAULT_API_URL = "https://botskill.ai";
|
|
7
|
+
export const FALLBACK_API_URL = 'http://localhost:3001/api';
|
|
8
|
+
|
|
9
|
+
export const getDefaultApiUrl = () =>
|
|
10
|
+
DEFAULT_API_URL === '__DEFAULT_API_URL__' ? FALLBACK_API_URL : DEFAULT_API_URL;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { createApiClient, getToken } from './auth.js';
|
|
4
|
+
|
|
5
|
+
const validCategories = ['ai', 'data', 'web', 'devops', 'security', 'tools'];
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Find uploadable file in cwd
|
|
9
|
+
* Priority: SKILL.md, skill.zip, skill.tar.gz, dist.zip
|
|
10
|
+
*/
|
|
11
|
+
export async function findUploadFile(cwd = process.cwd()) {
|
|
12
|
+
const candidates = [
|
|
13
|
+
path.join(cwd, 'SKILL.md'),
|
|
14
|
+
path.join(cwd, 'skill.zip'),
|
|
15
|
+
path.join(cwd, 'skill.tar.gz'),
|
|
16
|
+
path.join(cwd, 'dist.zip'),
|
|
17
|
+
];
|
|
18
|
+
for (const p of candidates) {
|
|
19
|
+
if (await fs.pathExists(p)) {
|
|
20
|
+
const stat = await fs.stat(p);
|
|
21
|
+
if (stat.isFile()) return p;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Upload skill file (SKILL.md, .zip, .tar.gz) to BotSkill
|
|
29
|
+
* @param {string} filePath - Path to file
|
|
30
|
+
*/
|
|
31
|
+
export async function uploadSkillFile(filePath) {
|
|
32
|
+
const token = getToken();
|
|
33
|
+
if (!token) {
|
|
34
|
+
throw new Error('NOT_LOGGED_IN');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!await fs.pathExists(filePath)) {
|
|
38
|
+
throw new Error('FILE_NOT_FOUND');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const FormData = (await import('form-data')).default;
|
|
42
|
+
const form = new FormData();
|
|
43
|
+
form.append('file', await fs.createReadStream(filePath), {
|
|
44
|
+
filename: path.basename(filePath),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const api = createApiClient();
|
|
48
|
+
const res = await api.post('/skills/upload', form, {
|
|
49
|
+
headers: form.getHeaders(),
|
|
50
|
+
maxBodyLength: Infinity,
|
|
51
|
+
maxContentLength: Infinity,
|
|
52
|
+
});
|
|
53
|
+
return res.data?.skill || res.data;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Legacy: upload via JSON (for backward compatibility, may be deprecated)
|
|
58
|
+
*/
|
|
59
|
+
export async function uploadSkill(options = {}) {
|
|
60
|
+
const token = getToken();
|
|
61
|
+
if (!token) throw new Error('NOT_LOGGED_IN');
|
|
62
|
+
|
|
63
|
+
let config = {};
|
|
64
|
+
const configPath = path.join(process.cwd(), 'skill.config.json');
|
|
65
|
+
if (await fs.pathExists(configPath)) {
|
|
66
|
+
config = await fs.readJson(configPath);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const skillData = {
|
|
70
|
+
name: options.name || config.name,
|
|
71
|
+
description: options.description || config.description,
|
|
72
|
+
version: options.version || config.version || '1.0.0',
|
|
73
|
+
category: options.category || config.category || 'tools',
|
|
74
|
+
tags: config.tags || [],
|
|
75
|
+
license: config.license || 'MIT',
|
|
76
|
+
repositoryUrl: config.repositoryUrl || undefined,
|
|
77
|
+
documentationUrl: config.documentationUrl || undefined,
|
|
78
|
+
demoUrl: config.demoUrl || undefined,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
if (!skillData.name || !skillData.description) throw new Error('MISSING_FIELDS');
|
|
82
|
+
if (!validCategories.includes(skillData.category)) {
|
|
83
|
+
throw new Error(`Invalid category. Must be one of: ${validCategories.join(', ')}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const api = createApiClient();
|
|
87
|
+
const res = await api.post('/skills', skillData);
|
|
88
|
+
return res.data?.skill || res.data;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export { validCategories };
|