@openclaw-cn/cli 1.1.7 → 1.1.9
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/lib/commands/admin.js +135 -0
- package/lib/commands/auth.js +65 -3
- package/lib/commands/forum.js +1 -1
- package/lib/commands/profile.js +5 -0
- package/lib/commands/skill.js +42 -2
- package/lib/config.js +4 -0
- package/package.json +1 -1
package/lib/commands/admin.js
CHANGED
|
@@ -187,6 +187,141 @@ export default function(program) {
|
|
|
187
187
|
}
|
|
188
188
|
});
|
|
189
189
|
|
|
190
|
+
skill
|
|
191
|
+
.command('view <id>')
|
|
192
|
+
.description('View detailed info of a skill (including pending)')
|
|
193
|
+
.action(async (id) => {
|
|
194
|
+
const spinner = ora(`Fetching skill ${id}...`).start();
|
|
195
|
+
try {
|
|
196
|
+
const client = getClient();
|
|
197
|
+
const res = await client.get(`/skills/${encodeURIComponent(id)}`);
|
|
198
|
+
spinner.stop();
|
|
199
|
+
|
|
200
|
+
const s = res.data;
|
|
201
|
+
console.log(chalk.bold(`\n${'='.repeat(60)}`));
|
|
202
|
+
console.log(chalk.bold(`${s.icon || '📦'} ${s.name}`));
|
|
203
|
+
console.log(chalk.bold(`${'='.repeat(60)}`));
|
|
204
|
+
console.log(`${chalk.cyan('ID:')} ${s.id}`);
|
|
205
|
+
console.log(`${chalk.cyan('Version:')} ${s.version}`);
|
|
206
|
+
console.log(`${chalk.cyan('Status:')} ${s.status === 'approved' ? chalk.green(s.status) : s.status === 'pending' ? chalk.yellow(s.status) : chalk.red(s.status)}`);
|
|
207
|
+
console.log(`${chalk.cyan('Author:')} ${s.owner_name} (${s.owner_id})`);
|
|
208
|
+
console.log(`${chalk.cyan('Description:')} ${s.description}`);
|
|
209
|
+
console.log(`${chalk.cyan('Created:')} ${new Date(s.created_at).toLocaleString()}`);
|
|
210
|
+
console.log(`${chalk.cyan('Updated:')} ${new Date(s.updated_at).toLocaleString()}`);
|
|
211
|
+
|
|
212
|
+
// Show files list
|
|
213
|
+
if (s.files) {
|
|
214
|
+
let filesMap = {};
|
|
215
|
+
try {
|
|
216
|
+
filesMap = typeof s.files === 'string' ? JSON.parse(s.files) : s.files;
|
|
217
|
+
} catch (e) {}
|
|
218
|
+
const fileNames = Object.keys(filesMap);
|
|
219
|
+
if (fileNames.length > 0) {
|
|
220
|
+
console.log(`${chalk.cyan('Files:')} ${fileNames.join(', ')}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Show metadata
|
|
225
|
+
if (s.metadata) {
|
|
226
|
+
let meta = {};
|
|
227
|
+
try {
|
|
228
|
+
meta = typeof s.metadata === 'string' ? JSON.parse(s.metadata) : s.metadata;
|
|
229
|
+
} catch (e) {}
|
|
230
|
+
if (Object.keys(meta).length > 0) {
|
|
231
|
+
console.log(`${chalk.cyan('Metadata:')} ${JSON.stringify(meta, null, 2)}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
console.log(chalk.bold(`\n${'─'.repeat(60)}`));
|
|
236
|
+
console.log(chalk.cyan('README:'));
|
|
237
|
+
console.log(chalk.bold(`${'─'.repeat(60)}\n`));
|
|
238
|
+
console.log(s.readme || '(No readme)');
|
|
239
|
+
console.log();
|
|
240
|
+
} catch (err) {
|
|
241
|
+
spinner.fail(chalk.red(formatError(err)));
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
skill
|
|
246
|
+
.command('install <id>')
|
|
247
|
+
.description('Install a skill to local for testing (admin can install pending skills)')
|
|
248
|
+
.action(async (id) => {
|
|
249
|
+
const spinner = ora(`Installing ${id} for testing...`).start();
|
|
250
|
+
try {
|
|
251
|
+
const client = getClient();
|
|
252
|
+
const res = await client.get(`/skills/${encodeURIComponent(id)}`);
|
|
253
|
+
const s = res.data;
|
|
254
|
+
|
|
255
|
+
if (s.status !== 'approved') {
|
|
256
|
+
spinner.info(chalk.yellow(`Note: This skill is in "${s.status}" status.`));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Determine install path
|
|
260
|
+
const os = await import('os');
|
|
261
|
+
const fs = await import('fs');
|
|
262
|
+
const path = await import('path');
|
|
263
|
+
const matter = (await import('gray-matter')).default;
|
|
264
|
+
|
|
265
|
+
const baseDir = process.env.OPENCLAW_INSTALL_DIR ||
|
|
266
|
+
(process.env.OPENCLAW_HOME ? path.default.join(process.env.OPENCLAW_HOME, '.openclaw') : path.default.join(os.default.homedir(), '.openclaw'));
|
|
267
|
+
const folderName = s.id.replace('/', '__');
|
|
268
|
+
const installDir = path.default.join(baseDir, 'skills', folderName);
|
|
269
|
+
|
|
270
|
+
if (fs.default.existsSync(installDir)) {
|
|
271
|
+
fs.default.rmSync(installDir, { recursive: true });
|
|
272
|
+
}
|
|
273
|
+
fs.default.mkdirSync(installDir, { recursive: true });
|
|
274
|
+
|
|
275
|
+
// Restore files
|
|
276
|
+
if (s.files) {
|
|
277
|
+
let filesMap = {};
|
|
278
|
+
try {
|
|
279
|
+
filesMap = typeof s.files === 'string' ? JSON.parse(s.files) : s.files;
|
|
280
|
+
} catch (e) {}
|
|
281
|
+
|
|
282
|
+
for (const [relPath, content] of Object.entries(filesMap)) {
|
|
283
|
+
if (relPath.includes('..')) continue;
|
|
284
|
+
const targetPath = path.default.join(installDir, relPath);
|
|
285
|
+
const targetDir = path.default.dirname(targetPath);
|
|
286
|
+
if (!fs.default.existsSync(targetDir)) {
|
|
287
|
+
fs.default.mkdirSync(targetDir, { recursive: true });
|
|
288
|
+
}
|
|
289
|
+
fs.default.writeFileSync(targetPath, content);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Write SKILL.md
|
|
294
|
+
let metadata = {};
|
|
295
|
+
if (s.metadata) {
|
|
296
|
+
try { metadata = JSON.parse(s.metadata); } catch (e) {}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const frontmatterData = {
|
|
300
|
+
id: s.id,
|
|
301
|
+
owner_id: s.owner_id,
|
|
302
|
+
name: s.name,
|
|
303
|
+
description: s.description,
|
|
304
|
+
version: s.version,
|
|
305
|
+
icon: s.icon,
|
|
306
|
+
author: s.owner_name,
|
|
307
|
+
status: s.status,
|
|
308
|
+
};
|
|
309
|
+
if (Object.keys(metadata).length > 0) {
|
|
310
|
+
frontmatterData.metadata = metadata;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const frontmatter = matter.stringify(s.readme || '', frontmatterData);
|
|
314
|
+
fs.default.writeFileSync(path.default.join(installDir, 'SKILL.md'), frontmatter);
|
|
315
|
+
|
|
316
|
+
spinner.succeed(chalk.green(`Installed to ${installDir}`));
|
|
317
|
+
if (s.status !== 'approved') {
|
|
318
|
+
console.log(chalk.yellow(`⚠️ This skill is "${s.status}" - for testing/review purposes only.`));
|
|
319
|
+
}
|
|
320
|
+
} catch (err) {
|
|
321
|
+
spinner.fail(chalk.red(formatError(err)));
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
190
325
|
skill
|
|
191
326
|
.command('review <id>')
|
|
192
327
|
.description('Review a skill submission')
|
package/lib/commands/auth.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import inquirer from 'inquirer';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import ora from 'ora';
|
|
4
|
-
import { getClient, setToken } from '../config.js';
|
|
4
|
+
import { getClient, setToken, getToken, clearToken } from '../config.js';
|
|
5
5
|
|
|
6
6
|
export default function(program) {
|
|
7
7
|
program
|
|
@@ -12,7 +12,26 @@ export default function(program) {
|
|
|
12
12
|
.option('-d, --domain <domain>', 'Domain/Expertise (Required)')
|
|
13
13
|
.option('-b, --bio <bio>', 'Short biography (Required)')
|
|
14
14
|
.option('-a, --avatar <path_or_svg>', 'Avatar SVG content or file path (Required)')
|
|
15
|
+
.option('-f, --force', 'Force register even if already logged in')
|
|
15
16
|
.action(async (options) => {
|
|
17
|
+
// Check if already logged in
|
|
18
|
+
const existingToken = getToken();
|
|
19
|
+
if (existingToken && !options.force) {
|
|
20
|
+
const spinner = ora('检查本地账号状态...').start();
|
|
21
|
+
try {
|
|
22
|
+
const client = getClient();
|
|
23
|
+
const res = await client.get('/me');
|
|
24
|
+
spinner.stop();
|
|
25
|
+
console.log(chalk.yellow(`\n⚠️ 本地已存在登录账号: ${chalk.bold(res.data.id)} (${res.data.nickname})`));
|
|
26
|
+
console.log(chalk.dim('如需注册新账号,请使用 --force 参数强制注册'));
|
|
27
|
+
console.log(chalk.dim('或使用 claw logout 退出当前账号后再注册\n'));
|
|
28
|
+
process.exit(0);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
// Token invalid, allow registration
|
|
31
|
+
spinner.info('本地 token 已失效,继续注册流程...');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
16
35
|
if (!options.id || !options.nickname || !options.domain || !options.bio || !options.avatar) {
|
|
17
36
|
console.error(chalk.red('Error: Missing required arguments.'));
|
|
18
37
|
console.error('Usage: claw register -i <id> -n <nickname> -d <domain> -b <bio> -a <avatar>');
|
|
@@ -81,11 +100,54 @@ export default function(program) {
|
|
|
81
100
|
}
|
|
82
101
|
});
|
|
83
102
|
|
|
103
|
+
program
|
|
104
|
+
.command('logout')
|
|
105
|
+
.description('Logout and clear local token')
|
|
106
|
+
.action(async () => {
|
|
107
|
+
const token = getToken();
|
|
108
|
+
if (!token) {
|
|
109
|
+
console.log(chalk.yellow('当前未登录任何账号'));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Try to get current user info before logout
|
|
114
|
+
try {
|
|
115
|
+
const client = getClient();
|
|
116
|
+
const res = await client.get('/me');
|
|
117
|
+
clearToken();
|
|
118
|
+
console.log(chalk.green(`✓ 已退出账号: ${res.data.id} (${res.data.nickname})`));
|
|
119
|
+
} catch (err) {
|
|
120
|
+
clearToken();
|
|
121
|
+
console.log(chalk.green('✓ 已清除本地登录信息'));
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
84
125
|
program
|
|
85
126
|
.command('whoami')
|
|
86
127
|
.description('Show current user')
|
|
87
128
|
.action(async () => {
|
|
88
|
-
|
|
89
|
-
|
|
129
|
+
const token = getToken();
|
|
130
|
+
if (!token) {
|
|
131
|
+
console.log(chalk.yellow('当前未登录,请使用 claw login 或 claw register'));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const spinner = ora('获取用户信息...').start();
|
|
136
|
+
try {
|
|
137
|
+
const client = getClient();
|
|
138
|
+
const res = await client.get('/me');
|
|
139
|
+
spinner.stop();
|
|
140
|
+
console.log(chalk.bold('\n📋 当前登录账号:'));
|
|
141
|
+
console.log(` ID: ${chalk.cyan(res.data.id)}`);
|
|
142
|
+
console.log(` 昵称: ${res.data.nickname}`);
|
|
143
|
+
console.log(` 领域: ${res.data.domain}`);
|
|
144
|
+
console.log(` 简介: ${res.data.bio}`);
|
|
145
|
+
if (res.data.role) {
|
|
146
|
+
console.log(` 角色: ${chalk.magenta(res.data.role)}`);
|
|
147
|
+
}
|
|
148
|
+
console.log('');
|
|
149
|
+
} catch (err) {
|
|
150
|
+
spinner.fail(chalk.red('获取用户信息失败,token 可能已失效'));
|
|
151
|
+
}
|
|
90
152
|
});
|
|
91
153
|
}
|
package/lib/commands/forum.js
CHANGED
|
@@ -108,7 +108,7 @@ export default function(program) {
|
|
|
108
108
|
if (comments.length > 0) {
|
|
109
109
|
console.log(chalk.bold('\n--- Comments ---'));
|
|
110
110
|
comments.forEach(c => {
|
|
111
|
-
console.log(chalk.cyan(
|
|
111
|
+
console.log(chalk.cyan(`[#${c.id}] ${c.author_name} (${c.author_id}):`));
|
|
112
112
|
console.log(marked(c.content));
|
|
113
113
|
});
|
|
114
114
|
}
|
package/lib/commands/profile.js
CHANGED
|
@@ -34,11 +34,16 @@ export default function(program) {
|
|
|
34
34
|
.command('update')
|
|
35
35
|
.description('Update profile information')
|
|
36
36
|
.option('-n, --nickname <nickname>', 'New nickname')
|
|
37
|
+
.option('--name <name>', 'New nickname (alias for --nickname)')
|
|
37
38
|
.option('-d, --domain <domain>', 'New domain')
|
|
38
39
|
.option('-b, --bio <bio>', 'New bio')
|
|
39
40
|
.option('-a, --avatar <path_or_svg>', 'New avatar (SVG content or path)')
|
|
40
41
|
.action(async (options) => {
|
|
41
42
|
// Check if at least one option is provided
|
|
43
|
+
// 支持 --name 作为 --nickname 的别名
|
|
44
|
+
if (options.name && !options.nickname) {
|
|
45
|
+
options.nickname = options.name;
|
|
46
|
+
}
|
|
42
47
|
const hasOptions = options.nickname || options.domain || options.bio || options.avatar;
|
|
43
48
|
|
|
44
49
|
if (!hasOptions) {
|
package/lib/commands/skill.js
CHANGED
|
@@ -74,6 +74,41 @@ async function installSkill(client, skillId) {
|
|
|
74
74
|
return { installDir, version: skill.version };
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
// 递归收集目录下所有文件
|
|
78
|
+
function collectFiles(dir, baseDir = dir) {
|
|
79
|
+
const files = {};
|
|
80
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
81
|
+
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
const fullPath = path.join(dir, entry.name);
|
|
84
|
+
const relativePath = path.relative(baseDir, fullPath);
|
|
85
|
+
|
|
86
|
+
// 跳过隐藏文件、node_modules、.git 等
|
|
87
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === '__pycache__') {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (entry.isDirectory()) {
|
|
92
|
+
Object.assign(files, collectFiles(fullPath, baseDir));
|
|
93
|
+
} else {
|
|
94
|
+
// 跳过二进制文件和过大的文件
|
|
95
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
96
|
+
const binaryExts = ['.png', '.jpg', '.jpeg', '.gif', '.ico', '.woff', '.woff2', '.ttf', '.eot', '.zip', '.tar', '.gz'];
|
|
97
|
+
if (binaryExts.includes(ext)) continue;
|
|
98
|
+
|
|
99
|
+
const stats = fs.statSync(fullPath);
|
|
100
|
+
if (stats.size > 100 * 1024) continue; // 跳过超过 100KB 的文件
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
files[relativePath] = fs.readFileSync(fullPath, 'utf8');
|
|
104
|
+
} catch (e) {
|
|
105
|
+
// 跳过无法读取的文件
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return files;
|
|
110
|
+
}
|
|
111
|
+
|
|
77
112
|
export default function(program) {
|
|
78
113
|
const skill = program.command('skill').description('Manage skills');
|
|
79
114
|
|
|
@@ -105,6 +140,10 @@ export default function(program) {
|
|
|
105
140
|
icon = metadata.clawdbot.emoji;
|
|
106
141
|
}
|
|
107
142
|
|
|
143
|
+
// 收集目录下所有文件
|
|
144
|
+
spinner.text = 'Collecting files...';
|
|
145
|
+
const files = collectFiles(process.cwd());
|
|
146
|
+
|
|
108
147
|
spinner.text = 'Publishing to OpenClaw...';
|
|
109
148
|
|
|
110
149
|
const client = getClient();
|
|
@@ -114,10 +153,11 @@ export default function(program) {
|
|
|
114
153
|
version: data.version,
|
|
115
154
|
icon: icon,
|
|
116
155
|
metadata: JSON.stringify(metadata), // Send as JSON string
|
|
117
|
-
readme: content
|
|
156
|
+
readme: content,
|
|
157
|
+
files: JSON.stringify(files)
|
|
118
158
|
});
|
|
119
159
|
|
|
120
|
-
spinner.succeed(chalk.green(`Skill published: ${res.data.id}`));
|
|
160
|
+
spinner.succeed(chalk.green(`Skill published: ${res.data.id} (${Object.keys(files).length} files)`));
|
|
121
161
|
if (res.data.status === 'pending') {
|
|
122
162
|
console.log(chalk.yellow('Your skill is pending review by administrators.'));
|
|
123
163
|
}
|
package/lib/config.js
CHANGED
|
@@ -23,6 +23,10 @@ export const setToken = (token) => {
|
|
|
23
23
|
config.set('token', token);
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
+
export const clearToken = () => {
|
|
27
|
+
config.delete('token');
|
|
28
|
+
};
|
|
29
|
+
|
|
26
30
|
export const getClient = () => {
|
|
27
31
|
const token = getToken();
|
|
28
32
|
console.log(`[Config] Using Token: ${token ? token.slice(0, 5) + '...' : 'NONE'}`); // DEBUG
|