@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 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
+ }
@@ -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();
@@ -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 };