istarshine 1.0.3 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +13 -0
- package/lib/api.js +34 -11
- package/lib/config.js +172 -0
- package/lib/install.js +57 -6
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ import { install_skill } from '../lib/install.js';
|
|
|
10
10
|
import { search_skills } from '../lib/search.js';
|
|
11
11
|
import { list_skills } from '../lib/list.js';
|
|
12
12
|
import { find_skills } from '../lib/find.js';
|
|
13
|
+
import { config_action } from '../lib/config.js';
|
|
13
14
|
|
|
14
15
|
const program = new Command();
|
|
15
16
|
|
|
@@ -24,6 +25,7 @@ program
|
|
|
24
25
|
.description('安装指定名称的 Skill 到当前项目')
|
|
25
26
|
.option('-d, --dir <path>', '安装目标目录', './skills')
|
|
26
27
|
.option('--server <url>', 'Skills Hub 服务器地址', 'https://skills.istarshine.com')
|
|
28
|
+
.option('--api-key <value>', '指定 API Key(覆盖环境变量和配置文件)')
|
|
27
29
|
.action(async (skill_name, options) => {
|
|
28
30
|
await install_skill(skill_name, options);
|
|
29
31
|
});
|
|
@@ -56,4 +58,15 @@ program
|
|
|
56
58
|
await find_skills(keyword, options);
|
|
57
59
|
});
|
|
58
60
|
|
|
61
|
+
// config 子命令
|
|
62
|
+
program
|
|
63
|
+
.command('config')
|
|
64
|
+
.description('管理 CLI 配置(API Key 等)')
|
|
65
|
+
.option('--api-key <value>', '设置 API Key')
|
|
66
|
+
.option('--show', '查看当前配置')
|
|
67
|
+
.option('--clear', '清除配置文件')
|
|
68
|
+
.action(async (options) => {
|
|
69
|
+
await config_action(options);
|
|
70
|
+
});
|
|
71
|
+
|
|
59
72
|
program.parse();
|
package/lib/api.js
CHANGED
|
@@ -9,17 +9,27 @@ import { request as http_request } from 'http';
|
|
|
9
9
|
/**
|
|
10
10
|
* 发送 GET 请求,返回 JSON
|
|
11
11
|
* @param {string} url - 完整请求 URL
|
|
12
|
+
* @param {object} headers - 可选的自定义请求头
|
|
12
13
|
* @returns {Promise<object>} 解析后的 JSON 对象
|
|
13
14
|
*/
|
|
14
|
-
export function fetch_json(url) {
|
|
15
|
+
export function fetch_json(url, headers = {}) {
|
|
15
16
|
return new Promise((resolve, reject) => {
|
|
16
|
-
const
|
|
17
|
+
const parsed = new URL(url);
|
|
18
|
+
const is_https = parsed.protocol === 'https:';
|
|
17
19
|
const do_request = is_https ? request : http_request;
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
const options = {
|
|
22
|
+
hostname: parsed.hostname,
|
|
23
|
+
port: parsed.port || (is_https ? 443 : 80),
|
|
24
|
+
path: parsed.pathname + parsed.search,
|
|
25
|
+
method: 'GET',
|
|
26
|
+
headers: { ...headers },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
do_request(options, (res) => {
|
|
20
30
|
// 处理重定向
|
|
21
31
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
22
|
-
return fetch_json(res.headers.location).then(resolve).catch(reject);
|
|
32
|
+
return fetch_json(res.headers.location, headers).then(resolve).catch(reject);
|
|
23
33
|
}
|
|
24
34
|
|
|
25
35
|
if (res.statusCode !== 200) {
|
|
@@ -41,19 +51,29 @@ export function fetch_json(url) {
|
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
/**
|
|
44
|
-
*
|
|
54
|
+
* 下载二进制文件,返回带 headers 属性的 Buffer
|
|
45
55
|
* @param {string} url - 完整请求 URL
|
|
46
|
-
* @
|
|
56
|
+
* @param {object} headers - 可选的自定义请求头
|
|
57
|
+
* @returns {Promise<Buffer>} 文件二进制数据,附带 .headers 属性(响应头)
|
|
47
58
|
*/
|
|
48
|
-
export function download_file(url) {
|
|
59
|
+
export function download_file(url, headers = {}) {
|
|
49
60
|
return new Promise((resolve, reject) => {
|
|
50
|
-
const
|
|
61
|
+
const parsed = new URL(url);
|
|
62
|
+
const is_https = parsed.protocol === 'https:';
|
|
51
63
|
const do_request = is_https ? request : http_request;
|
|
52
64
|
|
|
53
|
-
|
|
65
|
+
const options = {
|
|
66
|
+
hostname: parsed.hostname,
|
|
67
|
+
port: parsed.port || (is_https ? 443 : 80),
|
|
68
|
+
path: parsed.pathname + parsed.search,
|
|
69
|
+
method: 'GET',
|
|
70
|
+
headers: { ...headers },
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
do_request(options, (res) => {
|
|
54
74
|
// 处理重定向
|
|
55
75
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
56
|
-
return download_file(res.headers.location).then(resolve).catch(reject);
|
|
76
|
+
return download_file(res.headers.location, headers).then(resolve).catch(reject);
|
|
57
77
|
}
|
|
58
78
|
|
|
59
79
|
if (res.statusCode !== 200) {
|
|
@@ -64,7 +84,10 @@ export function download_file(url) {
|
|
|
64
84
|
const chunks = [];
|
|
65
85
|
res.on('data', (chunk) => { chunks.push(chunk); });
|
|
66
86
|
res.on('end', () => {
|
|
67
|
-
|
|
87
|
+
// 返回 Buffer 并附加响应头属性,便于调用方读取 X-Auth-Warning 等
|
|
88
|
+
const buffer = Buffer.concat(chunks);
|
|
89
|
+
Object.assign(buffer, { headers: res.headers });
|
|
90
|
+
resolve(buffer);
|
|
68
91
|
});
|
|
69
92
|
}).on('error', reject).end();
|
|
70
93
|
});
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 配置管理模块
|
|
3
|
+
* 负责 Config_File 的读写和 API Key 多来源优先级解析
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, writeFileSync, mkdirSync, unlinkSync, existsSync, chmodSync, statSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { homedir } from 'os';
|
|
9
|
+
|
|
10
|
+
// 配置目录和文件路径
|
|
11
|
+
const CONFIG_DIR = join(homedir(), '.istarshine');
|
|
12
|
+
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 读取配置文件,返回解析后的对象
|
|
16
|
+
* 文件不存在或 JSON 无效时返回 null
|
|
17
|
+
* @returns {object|null}
|
|
18
|
+
*/
|
|
19
|
+
export function read_config() {
|
|
20
|
+
try {
|
|
21
|
+
if (!existsSync(CONFIG_FILE)) return null;
|
|
22
|
+
const content = readFileSync(CONFIG_FILE, 'utf8');
|
|
23
|
+
return JSON.parse(content);
|
|
24
|
+
} catch {
|
|
25
|
+
// JSON 解析失败或读取失败,返回 null
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 写入配置文件(合并模式,保留已有字段)
|
|
32
|
+
* 自动创建 CONFIG_DIR,文件权限设为 0o600
|
|
33
|
+
* @param {object} updates - 要更新的字段
|
|
34
|
+
*/
|
|
35
|
+
export function write_config(updates) {
|
|
36
|
+
// 确保目录存在
|
|
37
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
38
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
// 读取已有配置并合并
|
|
41
|
+
const existing = read_config() || {};
|
|
42
|
+
const merged = { ...existing, ...updates };
|
|
43
|
+
// 写入文件,2 空格缩进
|
|
44
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), 'utf8');
|
|
45
|
+
// 设置文件权限为仅当前用户可读写
|
|
46
|
+
chmodSync(CONFIG_FILE, 0o600);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 删除配置文件
|
|
51
|
+
* @returns {boolean} 是否成功删除(文件不存在返回 false)
|
|
52
|
+
*/
|
|
53
|
+
export function clear_config() {
|
|
54
|
+
try {
|
|
55
|
+
if (!existsSync(CONFIG_FILE)) return false;
|
|
56
|
+
unlinkSync(CONFIG_FILE);
|
|
57
|
+
return true;
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 按优先级解析 API Key
|
|
65
|
+
* 1. cli_key(命令行 --api-key 参数)
|
|
66
|
+
* 2. 环境变量 ISTARSHINE_API_KEY
|
|
67
|
+
* 3. Config_File 中的 api_key 字段
|
|
68
|
+
* @param {string|undefined} cli_key - 命令行传入的 key
|
|
69
|
+
* @returns {string|null}
|
|
70
|
+
*/
|
|
71
|
+
export function resolve_api_key(cli_key) {
|
|
72
|
+
// 优先级 1:命令行参数
|
|
73
|
+
if (cli_key) return cli_key;
|
|
74
|
+
// 优先级 2:环境变量
|
|
75
|
+
const env_key = process.env.ISTARSHINE_API_KEY;
|
|
76
|
+
if (env_key) return env_key;
|
|
77
|
+
// 优先级 3:配置文件
|
|
78
|
+
const cfg = read_config();
|
|
79
|
+
if (cfg && cfg.api_key) return cfg.api_key;
|
|
80
|
+
// 所有来源均无 Key
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 将 API Key 脱敏为前缀形式
|
|
86
|
+
* 如 "sk-acme-a1b2c3d4..." → "sk-acme-a1b2..."
|
|
87
|
+
* @param {string} key
|
|
88
|
+
* @returns {string}
|
|
89
|
+
*/
|
|
90
|
+
export function mask_api_key(key) {
|
|
91
|
+
if (!key) return '';
|
|
92
|
+
// 格式: sk-{prefix}-{32hex}
|
|
93
|
+
// 找到最后一个 '-' 的位置,取前4位hex + '...'
|
|
94
|
+
const last_dash = key.lastIndexOf('-');
|
|
95
|
+
if (last_dash === -1) {
|
|
96
|
+
// 非标准格式,只显示前8字符
|
|
97
|
+
return key.slice(0, 8) + '...';
|
|
98
|
+
}
|
|
99
|
+
const before_hex = key.slice(0, last_dash + 1);
|
|
100
|
+
const hex_part = key.slice(last_dash + 1);
|
|
101
|
+
return before_hex + hex_part.slice(0, 4) + '...';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 导出路径常量,供测试使用
|
|
105
|
+
export { CONFIG_DIR, CONFIG_FILE };
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* config 子命令处理函数
|
|
109
|
+
* 支持 --api-key、--show、--clear 三个选项
|
|
110
|
+
* @param {object} options - 命令行选项
|
|
111
|
+
*/
|
|
112
|
+
export async function config_action(options) {
|
|
113
|
+
const chalk = (await import('chalk')).default;
|
|
114
|
+
|
|
115
|
+
// --clear:清除配置文件
|
|
116
|
+
if (options.clear) {
|
|
117
|
+
const removed = clear_config();
|
|
118
|
+
if (removed) {
|
|
119
|
+
console.log(chalk.green('✔ 配置文件已清除'));
|
|
120
|
+
} else {
|
|
121
|
+
console.log(chalk.yellow('配置文件不存在,无需清除'));
|
|
122
|
+
}
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// --api-key:写入 API Key
|
|
127
|
+
if (options.apiKey) {
|
|
128
|
+
try {
|
|
129
|
+
write_config({ api_key: options.apiKey });
|
|
130
|
+
const masked = mask_api_key(options.apiKey);
|
|
131
|
+
console.log(chalk.green(`✔ API Key 已保存: ${masked}`));
|
|
132
|
+
} catch (err) {
|
|
133
|
+
console.log(chalk.red(`✖ 写入配置失败: ${err.message}`));
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// --show:展示当前配置
|
|
140
|
+
if (options.show) {
|
|
141
|
+
const cfg = read_config();
|
|
142
|
+
const env_key = process.env.ISTARSHINE_API_KEY;
|
|
143
|
+
|
|
144
|
+
if (!cfg && !env_key) {
|
|
145
|
+
console.log(chalk.yellow('尚未配置。使用 istarshine config --api-key <value> 设置 API Key'));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
console.log(chalk.cyan('当前配置:'));
|
|
150
|
+
if (cfg && cfg.api_key) {
|
|
151
|
+
console.log(` API Key (配置文件): ${mask_api_key(cfg.api_key)}`);
|
|
152
|
+
}
|
|
153
|
+
if (env_key) {
|
|
154
|
+
console.log(` API Key (环境变量 ISTARSHINE_API_KEY): ${mask_api_key(env_key)}`);
|
|
155
|
+
}
|
|
156
|
+
if (cfg) {
|
|
157
|
+
// 展示其他非 api_key 字段
|
|
158
|
+
for (const [k, v] of Object.entries(cfg)) {
|
|
159
|
+
if (k !== 'api_key') {
|
|
160
|
+
console.log(` ${k}: ${v}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 无选项时显示帮助
|
|
168
|
+
console.log(chalk.yellow('请指定选项。用法:'));
|
|
169
|
+
console.log(' istarshine config --api-key <value> 设置 API Key');
|
|
170
|
+
console.log(' istarshine config --show 查看当前配置');
|
|
171
|
+
console.log(' istarshine config --clear 清除配置文件');
|
|
172
|
+
}
|
package/lib/install.js
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* install 命令实现
|
|
3
3
|
* 根据 skill 名称搜索、下载 zip 并解压到本地目录
|
|
4
|
+
* 支持自动携带 API Key 认证和 SKILL.md 声明检测
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import { fetch_json, download_file } from './api.js';
|
|
7
|
-
import {
|
|
8
|
+
import { readFileSync, mkdirSync, existsSync } from 'fs';
|
|
8
9
|
import { join, resolve } from 'path';
|
|
9
10
|
import { extract_zip } from './zip.js';
|
|
11
|
+
import { resolve_api_key, mask_api_key } from './config.js';
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* 安装指定名称的 skill
|
|
13
|
-
* 流程: 按名称搜索 -> 找到精确匹配 -> 下载 zip ->
|
|
15
|
+
* 流程: 按名称搜索 -> 找到精确匹配 -> 下载 zip(携带 API Key)-> 解压 -> 检查 SKILL.md 声明
|
|
14
16
|
* @param {string} skill_name - skill 名称
|
|
15
|
-
* @param {object} options - 命令行选项 { dir, server }
|
|
17
|
+
* @param {object} options - 命令行选项 { dir, server, apiKey }
|
|
16
18
|
*/
|
|
17
19
|
export async function install_skill(skill_name, options) {
|
|
18
20
|
const { dir, server } = options;
|
|
@@ -23,7 +25,14 @@ export async function install_skill(skill_name, options) {
|
|
|
23
25
|
const spinner = ora(`正在搜索 Skill: ${skill_name}`).start();
|
|
24
26
|
|
|
25
27
|
try {
|
|
26
|
-
//
|
|
28
|
+
// 解析 API Key(命令行 > 环境变量 > 配置文件)
|
|
29
|
+
const api_key = resolve_api_key(options.apiKey);
|
|
30
|
+
// 构建 auth headers,仅下载接口使用
|
|
31
|
+
const auth_headers = api_key
|
|
32
|
+
? { 'Authorization': `Bearer ${api_key}` }
|
|
33
|
+
: {};
|
|
34
|
+
|
|
35
|
+
// 1. 按名称搜索 skill(搜索接口不携带 auth header)
|
|
27
36
|
const search_url = `${server}/api/skills/search?keyword=${encodeURIComponent(skill_name)}`;
|
|
28
37
|
const results = await fetch_json(search_url);
|
|
29
38
|
|
|
@@ -40,10 +49,16 @@ export async function install_skill(skill_name, options) {
|
|
|
40
49
|
process.exit(1);
|
|
41
50
|
}
|
|
42
51
|
|
|
43
|
-
// 2. 下载 zip
|
|
52
|
+
// 2. 下载 zip 压缩包(下载接口携带 auth header)
|
|
44
53
|
spinner.text = `正在下载 ${skill_name}@${skill.version || 'latest'}...`;
|
|
45
54
|
const download_url = `${server}/api/skills/${skill.id}/download`;
|
|
46
|
-
const zip_buffer = await download_file(download_url);
|
|
55
|
+
const zip_buffer = await download_file(download_url, auth_headers);
|
|
56
|
+
|
|
57
|
+
// 检查响应头 X-Auth-Warning
|
|
58
|
+
if (zip_buffer.headers && zip_buffer.headers['x-auth-warning']) {
|
|
59
|
+
// 先不输出,等安装成功后再显示
|
|
60
|
+
var auth_warning = zip_buffer.headers['x-auth-warning'];
|
|
61
|
+
}
|
|
47
62
|
|
|
48
63
|
// 3. 解压到目标目录
|
|
49
64
|
const target_dir = resolve(process.cwd(), dir, skill_name);
|
|
@@ -58,8 +73,44 @@ export async function install_skill(skill_name, options) {
|
|
|
58
73
|
spinner.succeed(chalk.green(`${skill_name}@${skill.version || 'latest'} 安装成功`));
|
|
59
74
|
console.log(chalk.gray(` 安装路径: ${target_dir}`));
|
|
60
75
|
|
|
76
|
+
// 安装成功后输出 X-Auth-Warning 警告
|
|
77
|
+
if (auth_warning) {
|
|
78
|
+
console.log(chalk.yellow(` ⚠ ${auth_warning}`));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 4. 检查 SKILL.md 中的认证声明
|
|
82
|
+
check_skill_auth_declaration(target_dir, api_key, chalk);
|
|
83
|
+
|
|
61
84
|
} catch (err) {
|
|
62
85
|
spinner.fail(chalk.red(`安装失败: ${err.message}`));
|
|
63
86
|
process.exit(1);
|
|
64
87
|
}
|
|
65
88
|
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 检查解压后的 SKILL.md 是否包含 api.auth 或 metadata.openclaw 声明
|
|
92
|
+
* 如果包含且未配置 API Key,输出配置引导提示
|
|
93
|
+
* @param {string} target_dir - 解压目标目录
|
|
94
|
+
* @param {string|null} api_key - 当前解析到的 API Key
|
|
95
|
+
* @param {object} chalk - chalk 实例
|
|
96
|
+
*/
|
|
97
|
+
function check_skill_auth_declaration(target_dir, api_key, chalk) {
|
|
98
|
+
const skill_md_path = join(target_dir, 'SKILL.md');
|
|
99
|
+
if (!existsSync(skill_md_path)) return;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const content = readFileSync(skill_md_path, 'utf8');
|
|
103
|
+
// 检查是否包含认证声明
|
|
104
|
+
const has_api_auth = content.includes('api.auth') || content.includes('auth:');
|
|
105
|
+
const has_openclaw = content.includes('metadata.openclaw') || content.includes('primaryEnv');
|
|
106
|
+
|
|
107
|
+
if ((has_api_auth || has_openclaw) && !api_key) {
|
|
108
|
+
console.log('');
|
|
109
|
+
console.log(chalk.cyan(' 💡 该 Skill 需要 API Key 认证,请配置:'));
|
|
110
|
+
console.log(chalk.gray(' 方式一: npx istarshine config --api-key <your-key>'));
|
|
111
|
+
console.log(chalk.gray(' 方式二: openclaw config set skills.entries.<skill>.apiKey "<your-key>"'));
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
// 读取 SKILL.md 失败不影响安装流程
|
|
115
|
+
}
|
|
116
|
+
}
|