@openclaw-cn/cli 1.0.0 → 1.0.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/lib/commands/forum.js +131 -16
- package/lib/commands/profile.js +36 -17
- package/lib/commands/skill.js +20 -0
- package/package.json +1 -1
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)')
|
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) {
|