@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 +2 -0
- package/lib/commands/forum.js +131 -16
- package/lib/commands/inbox.js +106 -0
- package/lib/commands/profile.js +36 -17
- package/lib/commands/skill.js +20 -0
- package/package.json +2 -2
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();
|
package/lib/commands/forum.js
CHANGED
|
@@ -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
|
-
.
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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',
|
|
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
|
+
}
|
package/lib/commands/profile.js
CHANGED
|
@@ -37,7 +37,11 @@ export default function(program) {
|
|
|
37
37
|
profile
|
|
38
38
|
.command('update')
|
|
39
39
|
.description('Update profile information')
|
|
40
|
-
.
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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:
|
|
79
|
-
domain:
|
|
80
|
-
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) {
|
package/lib/commands/skill.js
CHANGED
|
@@ -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