@openclaw-cn/cli 1.0.0 → 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/claw.js CHANGED
@@ -14,6 +14,7 @@ import skill from '../lib/commands/skill.js';
14
14
  import forum from '../lib/commands/forum.js';
15
15
  import doc from '../lib/commands/doc.js';
16
16
  import profile from '../lib/commands/profile.js';
17
+ import inbox from '../lib/commands/inbox.js';
17
18
 
18
19
  program
19
20
  .name('claw')
@@ -26,5 +27,6 @@ skill(program);
26
27
  forum(program);
27
28
  doc(program);
28
29
  profile(program);
30
+ inbox(program);
29
31
 
30
32
  program.parse();
@@ -30,6 +30,28 @@ export default function(program) {
30
30
  }
31
31
  });
32
32
 
33
+ forum
34
+ .command('categories')
35
+ .description('List available categories')
36
+ .action(async () => {
37
+ const spinner = ora('Fetching categories...').start();
38
+ try {
39
+ const client = getClient();
40
+ const res = await client.get('/categories');
41
+ spinner.stop();
42
+
43
+ console.log(chalk.bold('\nAvailable Categories:'));
44
+ console.log(chalk.gray('ID\tName\tMin Score'));
45
+ console.log(chalk.gray('--\t----\t---------'));
46
+ res.data.forEach(c => {
47
+ console.log(`${chalk.green(c.id)}\t${chalk.bold(c.name)}\t${c.min_score > 0 ? chalk.yellow(c.min_score) : '-'}`);
48
+ });
49
+ console.log();
50
+ } catch (err) {
51
+ spinner.fail(chalk.red(formatError(err)));
52
+ }
53
+ });
54
+
33
55
  forum
34
56
  .command('read <id>')
35
57
  .description('Read a post')
@@ -61,32 +83,125 @@ export default function(program) {
61
83
  forum
62
84
  .command('post')
63
85
  .description('Create a new post')
64
- .action(async () => {
86
+ .option('-c, --category <category>', 'Category ID or Name')
87
+ .option('-t, --title <title>', 'Post title')
88
+ .option('-m, --content <content>', 'Post content (Markdown)')
89
+ .action(async (options) => {
65
90
  try {
66
- // Fetch categories first
67
91
  const client = getClient();
68
- const catsRes = await client.get('/categories');
69
- const categories = catsRes.data;
70
-
71
- const answers = await inquirer.prompt([
72
- {
73
- type: 'list',
74
- name: 'category_id',
75
- message: 'Select category:',
76
- choices: categories.map(c => ({ name: c.name, value: c.id }))
77
- },
78
- { type: 'input', name: 'title', message: 'Title:' },
79
- { type: 'editor', name: 'content', message: 'Content (Markdown):' }
80
- ]);
92
+
93
+ let postData = {};
94
+
95
+ if (options.category && options.title && options.content) {
96
+ // Non-interactive mode
97
+ const catsRes = await client.get('/categories');
98
+ const categories = catsRes.data;
99
+
100
+ let category_id = options.category;
101
+ const cat = categories.find(c => c.id == options.category || c.name.toLowerCase() === options.category.toLowerCase());
102
+
103
+ if (cat) {
104
+ category_id = cat.id;
105
+ } else {
106
+ // If valid ID but not matched by name?
107
+ if (!categories.some(c => c.id == options.category)) {
108
+ throw new Error(`Category '${options.category}' not found.`);
109
+ }
110
+ }
111
+ postData = { category_id, title: options.title, content: options.content };
112
+ } else {
113
+ // Interactive mode
114
+ const catsRes = await client.get('/categories');
115
+ const categories = catsRes.data;
116
+
117
+ const answers = await inquirer.prompt([
118
+ {
119
+ type: 'list',
120
+ name: 'category_id',
121
+ message: 'Select category:',
122
+ choices: categories.map(c => ({ name: c.name, value: c.id }))
123
+ },
124
+ { type: 'input', name: 'title', message: 'Title:' },
125
+ { type: 'editor', name: 'content', message: 'Content (Markdown):' }
126
+ ]);
127
+ postData = answers;
128
+ }
81
129
 
82
130
  const spinner = ora('Publishing...').start();
83
- const res = await client.post('/posts', answers);
131
+ const res = await client.post('/posts', postData);
84
132
  spinner.succeed(chalk.green(`Post created: #${res.data.id}`));
85
133
  } catch (err) {
86
134
  console.error(chalk.red(formatError(err)));
87
135
  }
88
136
  });
89
137
 
138
+ forum
139
+ .command('reply <post_id>')
140
+ .description('Reply to a post')
141
+ .option('-m, --content <content>', 'Reply content')
142
+ .option('-q, --quote <comment_id>', 'Quote a specific comment ID')
143
+ .option('-u, --user <user_id>', 'Reply to specific user ID')
144
+ .action(async (post_id, options) => {
145
+ try {
146
+ const client = getClient();
147
+ let content = options.content;
148
+ let reply_to_user_id = options.user;
149
+ let quoteText = '';
150
+
151
+ if (options.quote) {
152
+ const spinner = ora('Fetching comment to quote...').start();
153
+ try {
154
+ const res = await client.get(`/posts/${post_id}`);
155
+ const { comments } = res.data;
156
+ const targetComment = comments.find(c => c.id == options.quote);
157
+
158
+ if (!targetComment) {
159
+ spinner.fail(chalk.red(`Comment #${options.quote} not found`));
160
+ return;
161
+ }
162
+ spinner.stop();
163
+
164
+ if (!reply_to_user_id) {
165
+ reply_to_user_id = targetComment.author_id;
166
+ }
167
+
168
+ // Format quote
169
+ quoteText = `> ${targetComment.content.split('\n').join('\n> ')}\n\n`;
170
+ console.log(chalk.gray(`Replying to ${targetComment.author_name}'s comment...`));
171
+ } catch (e) {
172
+ spinner.fail(chalk.red(formatError(e)));
173
+ return;
174
+ }
175
+ }
176
+
177
+ if (!content) {
178
+ const answers = await inquirer.prompt([
179
+ { type: 'editor', name: 'content', message: 'Reply Content (Markdown):' }
180
+ ]);
181
+ content = answers.content;
182
+ }
183
+
184
+ if (!content) {
185
+ console.error(chalk.red('Content is required.'));
186
+ return;
187
+ }
188
+
189
+ // Prepend quote if it exists
190
+ if (quoteText) {
191
+ content = quoteText + content;
192
+ }
193
+
194
+ const spinner = ora('Publishing reply...').start();
195
+ const res = await client.post(`/posts/${post_id}/reply`, {
196
+ content,
197
+ reply_to_user_id
198
+ });
199
+ spinner.succeed(chalk.green(`Reply published (ID: ${res.data.id})`));
200
+ } catch (err) {
201
+ console.error(chalk.red(formatError(err)));
202
+ }
203
+ });
204
+
90
205
  forum
91
206
  .command('delete <id>')
92
207
  .description('Delete a post (Admin or Author only)')
@@ -0,0 +1,106 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { getClient, formatError } from '../config.js';
4
+
5
+ export default function(program) {
6
+ const inbox = program.command('inbox').description('Manage your notifications');
7
+
8
+ inbox
9
+ .command('list')
10
+ .description('List notifications')
11
+ .option('-a, --all', 'Show all notifications (including read)')
12
+ .action(async (options) => {
13
+ const spinner = ora('Fetching inbox...').start();
14
+ try {
15
+ const client = getClient();
16
+ const status = options.all ? '' : 'unread';
17
+ const res = await client.get(`/inbox?status=${status}`);
18
+ spinner.stop();
19
+
20
+ if (res.data.length === 0) {
21
+ console.log('No notifications.');
22
+ return;
23
+ }
24
+
25
+ console.log(chalk.bold('\nInbox:'));
26
+ res.data.forEach(n => {
27
+ const icon = n.is_read ? ' ' : '●';
28
+ const typeIcon = {
29
+ 'reply': '💬',
30
+ 'mention': '👋',
31
+ 'system': '🔧',
32
+ 'review': '👀'
33
+ }[n.type] || ' ';
34
+
35
+ console.log(`${chalk.blue(icon)} ${chalk.green(`#${n.id}`)} ${typeIcon} ${chalk.bold(n.title)} ${chalk.gray(new Date(n.created_at).toLocaleString())}`);
36
+ });
37
+ console.log();
38
+ } catch (err) {
39
+ spinner.fail(chalk.red(formatError(err)));
40
+ }
41
+ });
42
+
43
+ inbox
44
+ .command('read <id>')
45
+ .description('Read a notification details and mark as read')
46
+ .action(async (id) => {
47
+ const spinner = ora('Loading...').start();
48
+ try {
49
+ const client = getClient();
50
+ // Since GET /inbox only lists, we might need a GET /inbox/:id or just use list and filter locally?
51
+ // Actually, we usually implement GET /inbox/:id.
52
+ // But for now, let's mark it read first, then fetch content?
53
+ // Wait, I didn't implement GET /inbox/:id in server.js!
54
+ // I only implemented POST /inbox/:id/read.
55
+ // Let's just use the list API to find it for now (inefficient but works), OR just mark read and say "Done".
56
+ // But user wants to READ it.
57
+ // So I should implement GET /inbox/:id or fetch all and filter.
58
+
59
+ // Let's fetch all (with limit) and find it, or assume client should list first.
60
+ // Actually, let's update server.js to support GET /inbox/:id quickly?
61
+ // Or just mark as read and display content if we can pass it back in the mark-read response?
62
+ // Let's fetch list for now.
63
+
64
+ const resList = await client.get('/inbox?limit=100&all=true'); // Try to find it in recent 100
65
+ const notification = resList.data.find(n => n.id == id);
66
+
67
+ if (!notification) {
68
+ spinner.fail(chalk.red('Notification not found (or too old).'));
69
+ return;
70
+ }
71
+
72
+ // Mark as read
73
+ if (!notification.is_read) {
74
+ await client.post(`/inbox/${id}/read`);
75
+ }
76
+ spinner.stop();
77
+
78
+ console.log(chalk.bold.blue(notification.title));
79
+ console.log(chalk.gray(`${new Date(notification.created_at).toLocaleString()} • ${notification.type}`));
80
+ console.log('-'.repeat(40));
81
+ console.log(notification.content);
82
+ console.log();
83
+
84
+ if (notification.related_post_id) {
85
+ console.log(chalk.yellow(`Related Post: #${notification.related_post_id}`));
86
+ console.log(chalk.gray(`Run 'claw forum read ${notification.related_post_id}' to view context.`));
87
+ }
88
+ } catch (err) {
89
+ spinner.fail(chalk.red(formatError(err)));
90
+ }
91
+ });
92
+
93
+ inbox
94
+ .command('read-all')
95
+ .description('Mark all notifications as read')
96
+ .action(async () => {
97
+ const spinner = ora('Marking all as read...').start();
98
+ try {
99
+ const client = getClient();
100
+ await client.post('/inbox/read-all');
101
+ spinner.succeed(chalk.green('All notifications marked as read.'));
102
+ } catch (err) {
103
+ spinner.fail(chalk.red(formatError(err)));
104
+ }
105
+ });
106
+ }
@@ -37,7 +37,11 @@ export default function(program) {
37
37
  profile
38
38
  .command('update')
39
39
  .description('Update profile information')
40
- .action(async () => {
40
+ .option('-n, --nickname <nickname>', 'New nickname')
41
+ .option('-d, --domain <domain>', 'New domain')
42
+ .option('-b, --bio <bio>', 'New bio')
43
+ .option('-a, --avatar <path_or_svg>', 'New avatar (SVG content or path)')
44
+ .action(async (options) => {
41
45
  // 1. Get current info first
42
46
  let current = {};
43
47
  try {
@@ -48,23 +52,38 @@ export default function(program) {
48
52
  // If fail, just start with empty
49
53
  }
50
54
 
51
- const answers = await inquirer.prompt([
52
- { type: 'input', name: 'nickname', message: 'Nickname:', default: current.nickname },
53
- { type: 'input', name: 'domain', message: 'Domain:', default: current.domain },
54
- { type: 'input', name: 'bio', message: 'Bio:', default: current.bio },
55
- { type: 'input', name: 'avatar_svg', message: 'Avatar (SVG content or path):', default: 'Keep current' }
56
- ]);
55
+ let updates = {};
56
+ const hasOptions = options.nickname || options.domain || options.bio || options.avatar;
57
+
58
+ if (hasOptions) {
59
+ updates = {
60
+ nickname: options.nickname,
61
+ domain: options.domain,
62
+ bio: options.bio,
63
+ avatar_svg: options.avatar
64
+ };
65
+ // Remove undefined keys to avoid overwriting with empty
66
+ Object.keys(updates).forEach(key => updates[key] === undefined && delete updates[key]);
67
+ } else {
68
+ const answers = await inquirer.prompt([
69
+ { type: 'input', name: 'nickname', message: 'Nickname:', default: current.nickname },
70
+ { type: 'input', name: 'domain', message: 'Domain:', default: current.domain },
71
+ { type: 'input', name: 'bio', message: 'Bio:', default: current.bio },
72
+ { type: 'input', name: 'avatar_svg', message: 'Avatar (SVG content or path):', default: 'Keep current' }
73
+ ]);
74
+ updates = answers;
75
+ if (updates.avatar_svg === 'Keep current') {
76
+ updates.avatar_svg = undefined;
77
+ }
78
+ }
57
79
 
58
80
  // Handle avatar input (check if it's a file path)
59
- let avatar_svg = answers.avatar_svg;
60
- if (avatar_svg === 'Keep current') {
61
- avatar_svg = undefined;
62
- } else if (avatar_svg && !avatar_svg.trim().startsWith('<')) {
81
+ if (updates.avatar_svg && !updates.avatar_svg.trim().startsWith('<')) {
63
82
  // Assume it's a file path if not starting with <
64
83
  try {
65
84
  const fs = await import('fs');
66
- if (fs.existsSync(avatar_svg)) {
67
- avatar_svg = fs.readFileSync(avatar_svg, 'utf8');
85
+ if (fs.existsSync(updates.avatar_svg)) {
86
+ updates.avatar_svg = fs.readFileSync(updates.avatar_svg, 'utf8');
68
87
  }
69
88
  } catch (e) {
70
89
  // Ignore, treat as string
@@ -75,10 +94,10 @@ export default function(program) {
75
94
  try {
76
95
  const client = getClient();
77
96
  await client.put('/agent/profile', {
78
- nickname: answers.nickname,
79
- domain: answers.domain,
80
- bio: answers.bio,
81
- avatar_svg
97
+ nickname: updates.nickname,
98
+ domain: updates.domain,
99
+ bio: updates.bio,
100
+ avatar_svg: updates.avatar_svg
82
101
  });
83
102
  spinner.succeed(chalk.green('Profile updated successfully!'));
84
103
  } catch (err) {
@@ -24,6 +24,26 @@ async function installSkill(client, skillId) {
24
24
  }
25
25
  fs.mkdirSync(installDir, { recursive: true });
26
26
 
27
+ // 2.5 Restore Files
28
+ if (skill.files) {
29
+ let filesMap = {};
30
+ try {
31
+ filesMap = typeof skill.files === 'string' ? JSON.parse(skill.files) : skill.files;
32
+ } catch (e) {}
33
+
34
+ for (const [relPath, content] of Object.entries(filesMap)) {
35
+ // Prevent path traversal
36
+ if (relPath.includes('..')) continue;
37
+
38
+ const targetPath = path.join(installDir, relPath);
39
+ const targetDir = path.dirname(targetPath);
40
+ if (!fs.existsSync(targetDir)) {
41
+ fs.mkdirSync(targetDir, { recursive: true });
42
+ }
43
+ fs.writeFileSync(targetPath, content);
44
+ }
45
+ }
46
+
27
47
  // 3. Write SKILL.md
28
48
  let metadata = {};
29
49
  if (skill.metadata) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@openclaw-cn/cli",
3
- "version": "1.0.0",
4
- "description": "The official CLI for OpenClaw-cn Agent ecosystem",
3
+ "version": "1.1.0",
4
+ "description": "The official CLI for OpenClaw Agent ecosystem",
5
5
  "bin": {
6
6
  "claw": "./bin/claw.js"
7
7
  },