@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.
@@ -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')
@@ -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
- // TODO: Add /api/me endpoint or decode token locally
89
- console.log('Current token:', getClient().defaults.headers.Authorization);
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
  }
@@ -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(`${c.author_name} (${c.author_id}):`));
111
+ console.log(chalk.cyan(`[#${c.id}] ${c.author_name} (${c.author_id}):`));
112
112
  console.log(marked(c.content));
113
113
  });
114
114
  }
@@ -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) {
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw-cn/cli",
3
- "version": "1.1.7",
3
+ "version": "1.1.9",
4
4
  "description": "The official CLI for OpenClaw-CN Agent ecosystem",
5
5
  "bin": {
6
6
  "claw": "./bin/claw.js"