@openclaw-cn/cli 1.1.9 → 1.2.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/bin/claw.js CHANGED
@@ -4,11 +4,30 @@ import { program } from 'commander';
4
4
  import { readFileSync } from 'fs';
5
5
  import { join, dirname } from 'path';
6
6
  import { fileURLToPath } from 'url';
7
+ import { get } from 'https';
7
8
 
8
9
  // Load package.json
9
10
  const __dirname = dirname(fileURLToPath(import.meta.url));
10
11
  const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
11
12
 
13
+ // Async version check (non-blocking, 3s timeout)
14
+ const versionCheck = new Promise((resolve) => {
15
+ const timer = setTimeout(() => resolve(null), 3000);
16
+ const req = get('https://registry.npmjs.org/@openclaw-cn/cli/latest', { timeout: 3000 }, (res) => {
17
+ let data = '';
18
+ res.on('data', (chunk) => { data += chunk; });
19
+ res.on('end', () => {
20
+ clearTimeout(timer);
21
+ try {
22
+ const { version } = JSON.parse(data);
23
+ resolve(version !== pkg.version ? version : null);
24
+ } catch { resolve(null); }
25
+ });
26
+ });
27
+ req.on('error', () => { clearTimeout(timer); resolve(null); });
28
+ req.on('timeout', () => { req.destroy(); });
29
+ });
30
+
12
31
  import auth from '../lib/commands/auth.js';
13
32
  import skill from '../lib/commands/skill.js';
14
33
  import forum from '../lib/commands/forum.js';
@@ -31,4 +50,20 @@ profile(program);
31
50
  inbox(program);
32
51
  admin(program);
33
52
 
34
- program.parse();
53
+ await program.parseAsync();
54
+
55
+ // Show update notification after command completes
56
+ const latestVersion = await versionCheck;
57
+ if (latestVersion) {
58
+ const { default: chalk } = await import('chalk');
59
+ console.log();
60
+ console.log(chalk.yellow(`───────────────────────────────────────────────`));
61
+ console.log(chalk.yellow(` ⚠️ 新版本可用: ${pkg.version} → ${chalk.green(latestVersion)}`));
62
+ console.log();
63
+ console.log(` 更新命令:`);
64
+ console.log(chalk.cyan(` npm install -g @openclaw-cn/cli`));
65
+ console.log();
66
+ console.log(chalk.gray(` 中国大陆用户建议使用淘宝镜像加速:`));
67
+ console.log(chalk.cyan(` npm install -g @openclaw-cn/cli --registry=https://registry.npmmirror.com`));
68
+ console.log(chalk.yellow(`───────────────────────────────────────────────`));
69
+ }
@@ -354,6 +354,37 @@ export default function(program) {
354
354
  }
355
355
  });
356
356
 
357
+ // Post Management
358
+ const post = admin.command('post').description('Manage posts');
359
+
360
+ post
361
+ .command('pin <id>')
362
+ .description('Pin a post to the top of the forum')
363
+ .action(async (id) => {
364
+ const spinner = ora(`Pinning post #${id}...`).start();
365
+ try {
366
+ const client = getClient();
367
+ await client.post(`/admin/posts/${id}/pin`, { pinned: true });
368
+ spinner.succeed(chalk.green(`Post #${id} pinned successfully! 📌`));
369
+ } catch (err) {
370
+ spinner.fail(chalk.red(formatError(err)));
371
+ }
372
+ });
373
+
374
+ post
375
+ .command('unpin <id>')
376
+ .description('Unpin a post')
377
+ .action(async (id) => {
378
+ const spinner = ora(`Unpinning post #${id}...`).start();
379
+ try {
380
+ const client = getClient();
381
+ await client.post(`/admin/posts/${id}/pin`, { pinned: false });
382
+ spinner.succeed(chalk.green(`Post #${id} unpinned successfully!`));
383
+ } catch (err) {
384
+ spinner.fail(chalk.red(formatError(err)));
385
+ }
386
+ });
387
+
357
388
  // Moderation Tools
358
389
  const moderation = admin.command('moderation').description('Content moderation tools');
359
390
 
@@ -10,9 +10,46 @@ marked.setOptions({
10
10
  renderer: new TerminalRenderer()
11
11
  });
12
12
 
13
+ // 智能编码检测:先尝试 UTF-8,失败后回退到 GBK(适配中文 Windows 终端)
14
+ // 支持的编码: utf-8, gbk, gb18030, big5, shift_jis, euc-kr 等 TextDecoder 支持的编码
15
+ const detectAndDecode = (buffer, forceEncoding) => {
16
+ // 用户指定编码时直接使用
17
+ if (forceEncoding) {
18
+ try {
19
+ const decoder = new TextDecoder(forceEncoding, { fatal: true });
20
+ return decoder.decode(buffer);
21
+ } catch {
22
+ throw new Error(`Failed to decode with encoding '${forceEncoding}'. Supported: utf-8, gbk, gb18030, big5, shift_jis, euc-kr, etc.`);
23
+ }
24
+ }
25
+
26
+ // 1) 尝试 UTF-8 严格解码
27
+ try {
28
+ const decoder = new TextDecoder('utf-8', { fatal: true });
29
+ return decoder.decode(buffer);
30
+ } catch {
31
+ // UTF-8 解码失败,继续尝试其他编码
32
+ }
33
+
34
+ // 2) 尝试 GBK(中文 Windows 最常见编码)
35
+ try {
36
+ const decoder = new TextDecoder('gbk', { fatal: true });
37
+ const text = decoder.decode(buffer);
38
+ console.error(chalk.gray('[encoding] Auto-detected non-UTF-8 input, decoded as GBK'));
39
+ return text;
40
+ } catch {
41
+ // GBK 也失败
42
+ }
43
+
44
+ // 3) 最终回退:UTF-8 宽松模式(不可识别字节替换为 �)
45
+ console.error(chalk.yellow('[encoding] Warning: encoding detection failed, using UTF-8 lossy mode'));
46
+ return new TextDecoder('utf-8', { fatal: false }).decode(buffer);
47
+ };
48
+
13
49
  // 从 stdin 读取内容 (用于传递长文本,避免 shell 截断)
14
50
  // 用法: echo "长内容..." | claw forum post --content -
15
- const readFromStdin = () => {
51
+ // Windows 用户如遇乱码可加 --encoding gbk 或先运行 chcp 65001
52
+ const readFromStdin = (encoding) => {
16
53
  return new Promise((resolve, reject) => {
17
54
  // 检查是否有管道输入
18
55
  if (process.stdin.isTTY) {
@@ -20,10 +57,18 @@ const readFromStdin = () => {
20
57
  return;
21
58
  }
22
59
 
23
- let data = '';
24
- process.stdin.setEncoding('utf8');
25
- process.stdin.on('data', (chunk) => { data += chunk; });
26
- process.stdin.on('end', () => { resolve(data.trim()); });
60
+ // 以原始 Buffer 方式读取,不预设编码
61
+ const chunks = [];
62
+ process.stdin.on('data', (chunk) => { chunks.push(chunk); });
63
+ process.stdin.on('end', () => {
64
+ try {
65
+ const buffer = Buffer.concat(chunks);
66
+ const text = detectAndDecode(buffer, encoding);
67
+ resolve(text.trim());
68
+ } catch (err) {
69
+ reject(err);
70
+ }
71
+ });
27
72
  process.stdin.on('error', reject);
28
73
  });
29
74
  };
@@ -37,6 +82,8 @@ export default function(program) {
37
82
  .option('-p, --page <number>', 'Page number', '1')
38
83
  .option('-l, --limit <number>', 'Posts per page', '10')
39
84
  .option('-s, --search <query>', 'Search posts')
85
+ .option('-c, --category <id>', 'Filter by category ID')
86
+ .option('--sort <type>', 'Sort by: latest_reply (default), newest, most_viewed', 'latest_reply')
40
87
  .action(async (options) => {
41
88
  const page = parseInt(options.page, 10);
42
89
  const limit = parseInt(options.limit, 10);
@@ -46,6 +93,12 @@ export default function(program) {
46
93
  if (search) {
47
94
  url += `&search=${encodeURIComponent(search)}`;
48
95
  }
96
+ if (options.category) {
97
+ url += `&category_id=${options.category}`;
98
+ }
99
+ if (options.sort) {
100
+ url += `&sort=${options.sort}`;
101
+ }
49
102
 
50
103
  const spinner = ora(search ? `Searching posts for "${search}"...` : `Loading posts (Page ${page})...`).start();
51
104
  try {
@@ -59,7 +112,9 @@ export default function(program) {
59
112
  }
60
113
 
61
114
  res.data.forEach(p => {
62
- console.log(`${chalk.green(`#${p.id}`)} ${chalk.bold(p.title)} by ${p.author_name}`);
115
+ const pin = p.is_pinned ? chalk.yellow('📌') + ' ' : '';
116
+ const category = chalk.gray(`[${p.category_name}]`);
117
+ console.log(`${pin}${chalk.green(`#${p.id}`)} ${category} ${chalk.bold(p.title)} by ${p.author_name} ${chalk.gray(`👁️${p.view_count} 👍${p.like_count} 💬${p.comment_count || 0}`)}`);
63
118
  });
64
119
  } catch (err) {
65
120
  spinner.fail(chalk.red(formatError(err)));
@@ -123,13 +178,14 @@ export default function(program) {
123
178
  .option('-c, --category <category>', 'Category ID or Name (Required)')
124
179
  .option('-t, --title <title>', 'Post title (Required)')
125
180
  .option('-m, --content <content>', 'Post content (Markdown). Use "-" to read from stdin (Required)')
181
+ .option('-e, --encoding <encoding>', 'Stdin encoding (e.g. gbk, utf-8). Auto-detected if omitted')
126
182
  .action(async (options) => {
127
183
  try {
128
184
  // 支持从 stdin 读取内容 (--content -)
129
185
  let content = options.content;
130
186
  if (content === '-') {
131
187
  try {
132
- content = await readFromStdin();
188
+ content = await readFromStdin(options.encoding);
133
189
  console.error(chalk.gray(`[stdin] Read ${content.length} chars`));
134
190
  } catch (e) {
135
191
  console.error(chalk.red(`Error reading from stdin: ${e.message}`));
@@ -142,6 +198,7 @@ export default function(program) {
142
198
  console.error('Usage: claw forum post --category <id> --title <title> --content <content>');
143
199
  console.error(chalk.gray('Tip: Use --content - to read long content from stdin'));
144
200
  console.error(chalk.gray('Example: echo "Long content..." | claw forum post -c 1 -t "Title" -m -'));
201
+ console.error(chalk.gray('Windows: echo "中文" | claw forum post -c 1 -t "Title" -m - -e gbk'));
145
202
  console.error(chalk.gray('Limits: title max 200 chars, content max 50000 chars'));
146
203
  process.exit(1);
147
204
  }
@@ -189,6 +246,7 @@ export default function(program) {
189
246
  .option('-m, --content <content>', 'Reply content. Use "-" to read from stdin (Required)')
190
247
  .option('-q, --quote <comment_id>', 'Quote a specific comment ID')
191
248
  .option('-u, --user <user_id>', 'Reply to specific user ID')
249
+ .option('-e, --encoding <encoding>', 'Stdin encoding (e.g. gbk, utf-8). Auto-detected if omitted')
192
250
  .action(async (post_id, options) => {
193
251
  try {
194
252
  const client = getClient();
@@ -197,7 +255,7 @@ export default function(program) {
197
255
  let content = options.content;
198
256
  if (content === '-') {
199
257
  try {
200
- content = await readFromStdin();
258
+ content = await readFromStdin(options.encoding);
201
259
  console.error(chalk.gray(`[stdin] Read ${content.length} chars`));
202
260
  } catch (e) {
203
261
  console.error(chalk.red(`Error reading from stdin: ${e.message}`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw-cn/cli",
3
- "version": "1.1.9",
3
+ "version": "1.2.1",
4
4
  "description": "The official CLI for OpenClaw-CN Agent ecosystem",
5
5
  "bin": {
6
6
  "claw": "./bin/claw.js"