openclawmp 0.1.1 → 0.1.2
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/openclawmp.js +18 -10
- package/lib/api.js +65 -0
- package/lib/commands/comment.js +119 -0
- package/lib/commands/delete-account.js +44 -0
- package/lib/commands/install.js +14 -40
- package/lib/commands/issue.js +125 -0
- package/lib/commands/publish.js +50 -4
- package/lib/commands/star.js +67 -0
- package/lib/commands/unbind.js +37 -0
- package/lib/config.js +2 -2
- package/lib/help.js +16 -0
- package/package.json +1 -1
package/bin/openclawmp.js
CHANGED
|
@@ -16,16 +16,24 @@ const { printHelp } = require(path.join(libDir, 'help.js'));
|
|
|
16
16
|
// Command handlers (lazy-loaded)
|
|
17
17
|
const cmdDir = path.join(libDir, 'commands');
|
|
18
18
|
const commands = {
|
|
19
|
-
install:
|
|
20
|
-
uninstall:
|
|
21
|
-
search:
|
|
22
|
-
list:
|
|
23
|
-
info:
|
|
24
|
-
publish:
|
|
25
|
-
login:
|
|
26
|
-
authorize:
|
|
27
|
-
whoami:
|
|
28
|
-
|
|
19
|
+
install: () => require(path.join(cmdDir, 'install.js')),
|
|
20
|
+
uninstall: () => require(path.join(cmdDir, 'uninstall.js')),
|
|
21
|
+
search: () => require(path.join(cmdDir, 'search.js')),
|
|
22
|
+
list: () => require(path.join(cmdDir, 'list.js')),
|
|
23
|
+
info: () => require(path.join(cmdDir, 'info.js')),
|
|
24
|
+
publish: () => require(path.join(cmdDir, 'publish.js')),
|
|
25
|
+
login: () => require(path.join(cmdDir, 'login.js')),
|
|
26
|
+
authorize: () => require(path.join(cmdDir, 'login.js')), // alias
|
|
27
|
+
whoami: () => require(path.join(cmdDir, 'whoami.js')),
|
|
28
|
+
star: () => ({ run: (a, f) => require(path.join(cmdDir, 'star.js')).runStar(a, f) }),
|
|
29
|
+
unstar: () => ({ run: (a, f) => require(path.join(cmdDir, 'star.js')).runUnstar(a, f) }),
|
|
30
|
+
comment: () => ({ run: (a, f) => require(path.join(cmdDir, 'comment.js')).runComment(a, f) }),
|
|
31
|
+
comments: () => ({ run: (a, f) => require(path.join(cmdDir, 'comment.js')).runComments(a, f) }),
|
|
32
|
+
issue: () => ({ run: (a, f) => require(path.join(cmdDir, 'issue.js')).runIssue(a, f) }),
|
|
33
|
+
issues: () => ({ run: (a, f) => require(path.join(cmdDir, 'issue.js')).runIssues(a, f) }),
|
|
34
|
+
'delete-account': () => require(path.join(cmdDir, 'delete-account.js')),
|
|
35
|
+
unbind: () => require(path.join(cmdDir, 'unbind.js')),
|
|
36
|
+
help: () => ({ run: () => printHelp() }),
|
|
29
37
|
};
|
|
30
38
|
|
|
31
39
|
async function main() {
|
package/lib/api.js
CHANGED
|
@@ -152,11 +152,76 @@ async function findAsset(type, slug, authorFilter) {
|
|
|
152
152
|
return matches[0];
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Make a DELETE request to the API
|
|
157
|
+
* @param {string} apiPath
|
|
158
|
+
* @param {object} [body] - Optional JSON body
|
|
159
|
+
* @returns {Promise<{status: number, data: object}>}
|
|
160
|
+
*/
|
|
161
|
+
async function del(apiPath, body) {
|
|
162
|
+
const url = new URL(apiPath, config.getApiBase());
|
|
163
|
+
|
|
164
|
+
const opts = {
|
|
165
|
+
method: 'DELETE',
|
|
166
|
+
headers: { ...authHeaders() },
|
|
167
|
+
};
|
|
168
|
+
if (body) {
|
|
169
|
+
opts.headers['Content-Type'] = 'application/json';
|
|
170
|
+
opts.body = JSON.stringify(body);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const res = await fetch(url.toString(), opts);
|
|
174
|
+
const data = await res.json().catch(() => ({}));
|
|
175
|
+
return { status: res.status, data };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Resolve an asset reference to a full asset object.
|
|
180
|
+
* Accepts:
|
|
181
|
+
* - Direct ID: "tr-fc617094de29f938"
|
|
182
|
+
* - type/@author/slug: "trigger/@xiaoyue/pdf-watcher"
|
|
183
|
+
* @param {string} ref
|
|
184
|
+
* @returns {Promise<object>} asset object with at least { id, name, ... }
|
|
185
|
+
*/
|
|
186
|
+
async function resolveAssetRef(ref) {
|
|
187
|
+
// Direct ID pattern: known prefix + dash + hex (sk-/pl-/tr-/ch-/ex-)
|
|
188
|
+
if (/^(sk|pl|tr|ch|ex)-[0-9a-f]+$/.test(ref)) {
|
|
189
|
+
// Fetch by ID directly
|
|
190
|
+
const result = await get(`/api/assets/${ref}`);
|
|
191
|
+
// API may return { data: { asset } } or the asset directly
|
|
192
|
+
return result?.data?.asset || result?.data || result;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// type/@author/slug format
|
|
196
|
+
const parts = ref.split('/');
|
|
197
|
+
if (parts.length < 2) {
|
|
198
|
+
throw new Error(`Invalid asset reference: ${ref}. Use <id> or <type>/@<author>/<slug>`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const type = parts[0];
|
|
202
|
+
let slug, authorFilter = '';
|
|
203
|
+
|
|
204
|
+
if (parts.length >= 3 && parts[1].startsWith('@')) {
|
|
205
|
+
authorFilter = parts[1].slice(1);
|
|
206
|
+
slug = parts.slice(2).join('/');
|
|
207
|
+
} else {
|
|
208
|
+
slug = parts.slice(1).join('/');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const asset = await findAsset(type, slug, authorFilter);
|
|
212
|
+
if (!asset) {
|
|
213
|
+
throw new Error(`Asset not found: ${ref}`);
|
|
214
|
+
}
|
|
215
|
+
return asset;
|
|
216
|
+
}
|
|
217
|
+
|
|
155
218
|
module.exports = {
|
|
156
219
|
get,
|
|
157
220
|
post,
|
|
221
|
+
del,
|
|
158
222
|
postMultipart,
|
|
159
223
|
download,
|
|
160
224
|
searchAssets,
|
|
161
225
|
findAsset,
|
|
226
|
+
resolveAssetRef,
|
|
162
227
|
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// commands/comment.js — Post / list comments on an asset
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const api = require('../api.js');
|
|
8
|
+
const auth = require('../auth.js');
|
|
9
|
+
const { ok, info, err, c, detail } = require('../ui.js');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Format a timestamp to a readable date string
|
|
13
|
+
*/
|
|
14
|
+
function fmtDate(ts) {
|
|
15
|
+
if (!ts) return '?';
|
|
16
|
+
const d = new Date(ts);
|
|
17
|
+
if (isNaN(d.getTime())) return String(ts);
|
|
18
|
+
return d.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Render star rating: ★★★★☆
|
|
23
|
+
*/
|
|
24
|
+
function renderRating(rating) {
|
|
25
|
+
if (!rating) return '';
|
|
26
|
+
const n = Math.max(0, Math.min(5, Math.round(rating)));
|
|
27
|
+
return c('yellow', '★'.repeat(n)) + c('dim', '☆'.repeat(5 - n));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* openclawmp comment <assetRef> <content> [--rating N] [--as-agent]
|
|
32
|
+
*/
|
|
33
|
+
async function runComment(args, flags) {
|
|
34
|
+
if (args.length < 2) {
|
|
35
|
+
err('Usage: openclawmp comment <assetRef> <content> [--rating 5] [--as-agent]');
|
|
36
|
+
console.log(' Example: openclawmp comment trigger/@xiaoyue/pdf-watcher "非常好用!"');
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!auth.isAuthenticated()) {
|
|
41
|
+
err('Authentication required. Run: openclawmp login');
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const asset = await api.resolveAssetRef(args[0]);
|
|
46
|
+
const content = args.slice(1).join(' ');
|
|
47
|
+
const displayName = asset.displayName || asset.name || args[0];
|
|
48
|
+
|
|
49
|
+
const body = {
|
|
50
|
+
content,
|
|
51
|
+
commenterType: flags['as-agent'] ? 'agent' : 'user',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
if (flags.rating !== undefined) {
|
|
55
|
+
const rating = parseInt(flags.rating, 10);
|
|
56
|
+
if (isNaN(rating) || rating < 1 || rating > 5) {
|
|
57
|
+
err('Rating must be between 1 and 5');
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
body.rating = rating;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const { status, data } = await api.post(`/api/assets/${asset.id}/comments`, body);
|
|
64
|
+
|
|
65
|
+
if (status >= 200 && status < 300) {
|
|
66
|
+
const comment = data.comment || data;
|
|
67
|
+
console.log('');
|
|
68
|
+
ok(`评论已发布到 ${c('bold', displayName)}`);
|
|
69
|
+
if (body.rating) {
|
|
70
|
+
detail('评分', renderRating(body.rating));
|
|
71
|
+
}
|
|
72
|
+
detail('内容', content);
|
|
73
|
+
console.log('');
|
|
74
|
+
} else {
|
|
75
|
+
err(`评论失败 (${status}): ${data.error || data.message || JSON.stringify(data)}`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* openclawmp comments <assetRef>
|
|
82
|
+
*/
|
|
83
|
+
async function runComments(args) {
|
|
84
|
+
if (args.length === 0) {
|
|
85
|
+
err('Usage: openclawmp comments <assetRef>');
|
|
86
|
+
console.log(' Example: openclawmp comments trigger/@xiaoyue/pdf-watcher');
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const asset = await api.resolveAssetRef(args[0]);
|
|
91
|
+
const displayName = asset.displayName || asset.name || args[0];
|
|
92
|
+
|
|
93
|
+
const result = await api.get(`/api/assets/${asset.id}/comments`);
|
|
94
|
+
const comments = result?.data?.comments || result?.comments || [];
|
|
95
|
+
|
|
96
|
+
console.log('');
|
|
97
|
+
info(`${c('bold', displayName)} 的评论(${comments.length} 条)`);
|
|
98
|
+
console.log(` ${'─'.repeat(50)}`);
|
|
99
|
+
|
|
100
|
+
if (comments.length === 0) {
|
|
101
|
+
console.log(` ${c('dim', '暂无评论。成为第一个评论者吧!')}`);
|
|
102
|
+
console.log('');
|
|
103
|
+
console.log(` openclawmp comment ${args[0]} "你的评论"`);
|
|
104
|
+
} else {
|
|
105
|
+
for (const cm of comments) {
|
|
106
|
+
const author = cm.author?.name || cm.authorName || cm.commenterType || 'anonymous';
|
|
107
|
+
const rating = cm.rating ? ` ${renderRating(cm.rating)}` : '';
|
|
108
|
+
const badge = cm.commenterType === 'agent' ? c('magenta', ' 🤖') : '';
|
|
109
|
+
const time = fmtDate(cm.createdAt || cm.created_at);
|
|
110
|
+
|
|
111
|
+
console.log('');
|
|
112
|
+
console.log(` ${c('cyan', author)}${badge}${rating} ${c('dim', time)}`);
|
|
113
|
+
console.log(` ${cm.content}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
console.log('');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = { runComment, runComments };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// commands/delete-account.js — Delete (deactivate) account + unbind devices
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const api = require('../api.js');
|
|
8
|
+
const { fish, ok, err, warn, c, detail } = require('../ui.js');
|
|
9
|
+
|
|
10
|
+
async function run(args, flags) {
|
|
11
|
+
fish('Requesting account deletion...');
|
|
12
|
+
|
|
13
|
+
// Safety: require --confirm flag
|
|
14
|
+
if (!flags.confirm && !flags.yes && !flags.y) {
|
|
15
|
+
console.log('');
|
|
16
|
+
warn('This will permanently deactivate your account:');
|
|
17
|
+
console.log('');
|
|
18
|
+
console.log(' • 软删除账号(设置 deleted_at)');
|
|
19
|
+
console.log(' • 解绑所有设备');
|
|
20
|
+
console.log(' • 撤销所有 API Key');
|
|
21
|
+
console.log(' • 解除 OAuth 关联(GitHub/Google 可重新注册)');
|
|
22
|
+
console.log(' • 已发布的资产保留,不删除');
|
|
23
|
+
console.log('');
|
|
24
|
+
console.log(` To confirm, run: ${c('bold', 'openclawmp delete-account --confirm')}`);
|
|
25
|
+
console.log('');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { status, data } = await api.del('/api/auth/account');
|
|
30
|
+
|
|
31
|
+
if (status >= 200 && status < 300 && data?.success) {
|
|
32
|
+
console.log('');
|
|
33
|
+
ok('账号已注销');
|
|
34
|
+
console.log('');
|
|
35
|
+
detail('状态', '设备已解绑,API Key 已撤销,OAuth 已解除关联');
|
|
36
|
+
detail('资产', '已发布的资产仍会保留');
|
|
37
|
+
console.log('');
|
|
38
|
+
} else {
|
|
39
|
+
err(data?.message || data?.error || `Account deletion failed (HTTP ${status})`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { run };
|
package/lib/commands/install.js
CHANGED
|
@@ -96,31 +96,6 @@ function countFiles(dir) {
|
|
|
96
96
|
return count;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
/**
|
|
100
|
-
* Generate fallback SKILL.md from asset metadata
|
|
101
|
-
*/
|
|
102
|
-
function generateSkillMd(asset, targetDir) {
|
|
103
|
-
const tags = (asset.tags || []).join(', ');
|
|
104
|
-
const content = `---
|
|
105
|
-
name: ${asset.name}
|
|
106
|
-
display-name: ${asset.displayName || ''}
|
|
107
|
-
description: ${asset.description || ''}
|
|
108
|
-
version: ${asset.version}
|
|
109
|
-
author: ${asset.author?.name || ''}
|
|
110
|
-
author-id: ${asset.author?.id || ''}
|
|
111
|
-
tags: ${tags}
|
|
112
|
-
category: ${asset.category || ''}
|
|
113
|
-
---
|
|
114
|
-
|
|
115
|
-
# ${asset.displayName || asset.name}
|
|
116
|
-
|
|
117
|
-
${asset.description || ''}
|
|
118
|
-
|
|
119
|
-
${asset.readme || ''}
|
|
120
|
-
`;
|
|
121
|
-
fs.writeFileSync(path.join(targetDir, 'SKILL.md'), content);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
99
|
/**
|
|
125
100
|
* Write manifest.json for the installed asset
|
|
126
101
|
*/
|
|
@@ -145,26 +120,24 @@ function writeManifest(asset, targetDir, hasPackage) {
|
|
|
145
120
|
/**
|
|
146
121
|
* Post-install hints per asset type
|
|
147
122
|
*/
|
|
148
|
-
function showPostInstallHints(type, slug) {
|
|
123
|
+
function showPostInstallHints(type, slug, targetDir) {
|
|
149
124
|
switch (type) {
|
|
150
125
|
case 'skill':
|
|
151
126
|
console.log(` ${c('green', 'Ready!')} Will be loaded in the next agent session.`);
|
|
152
127
|
break;
|
|
153
|
-
case 'config':
|
|
154
|
-
console.log(` To activate: ${c('bold', `openclawmp apply config/${slug}`)}`);
|
|
155
|
-
break;
|
|
156
|
-
case 'trigger':
|
|
157
|
-
console.log(` ${c('yellow', 'Check dependencies:')} fswatch (macOS) / inotifywait (Linux)`);
|
|
158
|
-
console.log(' Quick start: see SKILL.md in the installed directory');
|
|
159
|
-
break;
|
|
160
128
|
case 'plugin':
|
|
161
129
|
console.log(` ${c('yellow', 'Requires restart:')} openclaw gateway restart`);
|
|
162
130
|
break;
|
|
163
131
|
case 'channel':
|
|
164
132
|
console.log(` ${c('yellow', 'Requires config:')} Set credentials in openclaw.json, then restart`);
|
|
165
133
|
break;
|
|
166
|
-
case '
|
|
167
|
-
console.log(`
|
|
134
|
+
case 'trigger':
|
|
135
|
+
console.log(` ${c('yellow', 'Manual setup:')} Read README.md for cron/heartbeat configuration`);
|
|
136
|
+
console.log(` ${c('dim', `cat ${targetDir}/README.md`)}`);
|
|
137
|
+
break;
|
|
138
|
+
case 'experience':
|
|
139
|
+
console.log(` ${c('yellow', 'Reference:')} Read README.md for setup instructions`);
|
|
140
|
+
console.log(` ${c('dim', `cat ${targetDir}/README.md`)}`);
|
|
168
141
|
break;
|
|
169
142
|
}
|
|
170
143
|
}
|
|
@@ -241,11 +214,12 @@ async function run(args, flags) {
|
|
|
241
214
|
}
|
|
242
215
|
}
|
|
243
216
|
|
|
244
|
-
//
|
|
217
|
+
// No package → error (no fallback generation)
|
|
245
218
|
if (!hasPackage) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
console.log(
|
|
219
|
+
try { fs.rmSync(targetDir, { recursive: true, force: true }); } catch {}
|
|
220
|
+
err('该资产没有可安装的 package。');
|
|
221
|
+
console.log(` 请在水产市场查看详情:${config.getApiBase()}/asset/${asset.id}`);
|
|
222
|
+
process.exit(1);
|
|
249
223
|
}
|
|
250
224
|
|
|
251
225
|
// Always write manifest.json
|
|
@@ -263,7 +237,7 @@ async function run(args, flags) {
|
|
|
263
237
|
detail('Command', `openclawmp install ${type}/@${authorId}/${slug}`);
|
|
264
238
|
console.log('');
|
|
265
239
|
|
|
266
|
-
showPostInstallHints(type, slug);
|
|
240
|
+
showPostInstallHints(type, slug, targetDir);
|
|
267
241
|
console.log('');
|
|
268
242
|
}
|
|
269
243
|
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// commands/issue.js — Create / list issues on an asset
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const api = require('../api.js');
|
|
8
|
+
const auth = require('../auth.js');
|
|
9
|
+
const { ok, info, err, c, detail } = require('../ui.js');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Format a timestamp to a readable date string
|
|
13
|
+
*/
|
|
14
|
+
function fmtDate(ts) {
|
|
15
|
+
if (!ts) return '?';
|
|
16
|
+
const d = new Date(ts);
|
|
17
|
+
if (isNaN(d.getTime())) return String(ts);
|
|
18
|
+
return d.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Render issue status with color
|
|
23
|
+
*/
|
|
24
|
+
function renderStatus(status) {
|
|
25
|
+
switch (status) {
|
|
26
|
+
case 'open': return c('green', '● open');
|
|
27
|
+
case 'closed': return c('red', '● closed');
|
|
28
|
+
default: return c('dim', status || 'open');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* openclawmp issue <assetRef> <title> [--body "..."] [--labels "bug,help"] [--as-agent]
|
|
34
|
+
*/
|
|
35
|
+
async function runIssue(args, flags) {
|
|
36
|
+
if (args.length < 2) {
|
|
37
|
+
err('Usage: openclawmp issue <assetRef> <title> [--body "..."] [--labels "bug,help"] [--as-agent]');
|
|
38
|
+
console.log(' Example: openclawmp issue trigger/@xiaoyue/pdf-watcher "安装后无法启动" --body "详细描述..."');
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!auth.isAuthenticated()) {
|
|
43
|
+
err('Authentication required. Run: openclawmp login');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const asset = await api.resolveAssetRef(args[0]);
|
|
48
|
+
const title = args.slice(1).join(' ');
|
|
49
|
+
const displayName = asset.displayName || asset.name || args[0];
|
|
50
|
+
|
|
51
|
+
const body = {
|
|
52
|
+
title,
|
|
53
|
+
authorType: flags['as-agent'] ? 'agent' : 'user',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
if (flags.body) {
|
|
57
|
+
body.bodyText = flags.body;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (flags.labels) {
|
|
61
|
+
body.labels = flags.labels.split(',').map(l => l.trim()).filter(Boolean);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const { status, data } = await api.post(`/api/assets/${asset.id}/issues`, body);
|
|
65
|
+
|
|
66
|
+
if (status >= 200 && status < 300) {
|
|
67
|
+
const issue = data.issue || data;
|
|
68
|
+
const issueNum = issue.number || issue.id || '?';
|
|
69
|
+
|
|
70
|
+
console.log('');
|
|
71
|
+
ok(`Issue #${issueNum} 已创建于 ${c('bold', displayName)}`);
|
|
72
|
+
detail('标题', title);
|
|
73
|
+
if (flags.body) {
|
|
74
|
+
detail('描述', flags.body.length > 60 ? flags.body.slice(0, 60) + '...' : flags.body);
|
|
75
|
+
}
|
|
76
|
+
if (flags.labels) {
|
|
77
|
+
detail('标签', flags.labels);
|
|
78
|
+
}
|
|
79
|
+
console.log('');
|
|
80
|
+
} else {
|
|
81
|
+
err(`创建 Issue 失败 (${status}): ${data.error || data.message || JSON.stringify(data)}`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* openclawmp issues <assetRef>
|
|
88
|
+
*/
|
|
89
|
+
async function runIssues(args) {
|
|
90
|
+
if (args.length === 0) {
|
|
91
|
+
err('Usage: openclawmp issues <assetRef>');
|
|
92
|
+
console.log(' Example: openclawmp issues trigger/@xiaoyue/pdf-watcher');
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const asset = await api.resolveAssetRef(args[0]);
|
|
97
|
+
const displayName = asset.displayName || asset.name || args[0];
|
|
98
|
+
|
|
99
|
+
const result = await api.get(`/api/assets/${asset.id}/issues`);
|
|
100
|
+
const issues = result?.data?.issues || result?.issues || [];
|
|
101
|
+
|
|
102
|
+
console.log('');
|
|
103
|
+
info(`${c('bold', displayName)} 的 Issues(${issues.length} 个)`);
|
|
104
|
+
console.log(` ${'─'.repeat(50)}`);
|
|
105
|
+
|
|
106
|
+
if (issues.length === 0) {
|
|
107
|
+
console.log(` ${c('dim', '暂无 Issues。')}`);
|
|
108
|
+
} else {
|
|
109
|
+
for (const iss of issues) {
|
|
110
|
+
const num = iss.number || iss.id || '?';
|
|
111
|
+
const status = renderStatus(iss.status);
|
|
112
|
+
const author = iss.author?.name || iss.authorName || iss.authorType || 'anonymous';
|
|
113
|
+
const badge = iss.authorType === 'agent' ? c('magenta', ' 🤖') : '';
|
|
114
|
+
const time = fmtDate(iss.createdAt || iss.created_at);
|
|
115
|
+
const labels = (iss.labels || []).map(l => c('yellow', `[${l}]`)).join(' ');
|
|
116
|
+
|
|
117
|
+
console.log('');
|
|
118
|
+
console.log(` ${status} ${c('bold', `#${num}`)} ${iss.title} ${labels}`);
|
|
119
|
+
console.log(` ${c('dim', `by ${author}${badge} · ${time}`)}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
console.log('');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = { runIssue, runIssues };
|
package/lib/commands/publish.js
CHANGED
|
@@ -186,11 +186,57 @@ async function run(args, flags) {
|
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
189
|
+
// ─── Validate package contents (hard block) ─────────────────────────
|
|
190
|
+
const valErrors = [];
|
|
191
|
+
switch (meta.type) {
|
|
192
|
+
case 'skill': {
|
|
193
|
+
const sp = path.join(skillDir, 'SKILL.md');
|
|
194
|
+
if (!fs.existsSync(sp)) { valErrors.push('缺少 SKILL.md — skill 类型必须包含此文件'); break; }
|
|
195
|
+
const { frontmatter: sfm, body: sbody } = parseFrontmatter(fs.readFileSync(sp, 'utf-8'));
|
|
196
|
+
if (!sfm.name && !sfm.displayName && !sfm['display-name']) valErrors.push('SKILL.md frontmatter 缺少 name');
|
|
197
|
+
if (!sfm.description) valErrors.push('SKILL.md frontmatter 缺少 description');
|
|
198
|
+
if (!sbody.trim()) valErrors.push('SKILL.md 正文为空(frontmatter 之后需要技能说明)');
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
case 'plugin':
|
|
202
|
+
case 'channel': {
|
|
203
|
+
const pjp = path.join(skillDir, 'openclaw.plugin.json');
|
|
204
|
+
if (!fs.existsSync(pjp)) { valErrors.push(`缺少 openclaw.plugin.json — ${meta.type} 类型必须包含此文件`); break; }
|
|
205
|
+
try {
|
|
206
|
+
const pd = JSON.parse(fs.readFileSync(pjp, 'utf-8'));
|
|
207
|
+
if (!pd.id) valErrors.push('openclaw.plugin.json 缺少 id');
|
|
208
|
+
if (meta.type === 'channel' && (!Array.isArray(pd.channels) || !pd.channels.length)) {
|
|
209
|
+
valErrors.push('openclaw.plugin.json 缺少 channels 数组(channel 类型必须声明)');
|
|
210
|
+
}
|
|
211
|
+
} catch { valErrors.push('openclaw.plugin.json JSON 格式错误'); break; }
|
|
212
|
+
if (!fs.existsSync(path.join(skillDir, 'README.md'))) valErrors.push(`缺少 README.md — ${meta.type} 类型必须包含 README.md`);
|
|
213
|
+
if (!meta.displayName || !meta.description) valErrors.push('无法提取 displayName/description — 请在 openclaw.plugin.json 添加 name/description 或确保 README.md 有标题和描述');
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
case 'trigger':
|
|
217
|
+
case 'experience': {
|
|
218
|
+
const rp = path.join(skillDir, 'README.md');
|
|
219
|
+
if (!fs.existsSync(rp)) { valErrors.push(`缺少 README.md — ${meta.type} 类型必须包含此文件`); break; }
|
|
220
|
+
const rc = fs.readFileSync(rp, 'utf-8');
|
|
221
|
+
let ht = false, hd = false;
|
|
222
|
+
for (const l of rc.split('\n')) {
|
|
223
|
+
const t = l.trim();
|
|
224
|
+
if (!ht && /^#\s+.+/.test(t)) { ht = true; continue; }
|
|
225
|
+
if (ht && !hd && t && !t.startsWith('#') && !t.startsWith('---') && !t.startsWith('>')) { hd = true; break; }
|
|
226
|
+
}
|
|
227
|
+
if (!ht) valErrors.push('README.md 缺少标题行(# 名称)');
|
|
228
|
+
if (!hd) valErrors.push('README.md 缺少描述段落(标题后需要有文字说明)');
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (valErrors.length) {
|
|
193
234
|
console.log('');
|
|
235
|
+
err('发布校验失败:');
|
|
236
|
+
for (const e of valErrors) console.log(` ${c('red', '✗')} ${e}`);
|
|
237
|
+
console.log('');
|
|
238
|
+
info('请补全以上内容后重新发布。');
|
|
239
|
+
process.exit(1);
|
|
194
240
|
}
|
|
195
241
|
|
|
196
242
|
// Show preview
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// commands/star.js — Star / unstar an asset
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const api = require('../api.js');
|
|
8
|
+
const auth = require('../auth.js');
|
|
9
|
+
const { ok, err, c } = require('../ui.js');
|
|
10
|
+
|
|
11
|
+
async function runStar(args) {
|
|
12
|
+
if (args.length === 0) {
|
|
13
|
+
err('Usage: openclawmp star <assetRef>');
|
|
14
|
+
console.log(' Example: openclawmp star trigger/@xiaoyue/pdf-watcher');
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!auth.isAuthenticated()) {
|
|
19
|
+
err('Authentication required. Run: openclawmp login');
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const asset = await api.resolveAssetRef(args[0]);
|
|
24
|
+
const displayName = asset.displayName || asset.name || args[0];
|
|
25
|
+
|
|
26
|
+
const { status, data } = await api.post(`/api/assets/${asset.id}/star`, {});
|
|
27
|
+
|
|
28
|
+
if (status >= 200 && status < 300) {
|
|
29
|
+
const totalStars = data.totalStars ?? data.stars ?? '?';
|
|
30
|
+
ok(`★ 已收藏 ${c('bold', displayName)}(共 ${c('cyan', String(totalStars))} 人收藏)`);
|
|
31
|
+
} else if (status === 409) {
|
|
32
|
+
// Already starred
|
|
33
|
+
ok(`★ 你已经收藏过 ${c('bold', displayName)} 了`);
|
|
34
|
+
} else {
|
|
35
|
+
err(`收藏失败 (${status}): ${data.error || data.message || JSON.stringify(data)}`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function runUnstar(args) {
|
|
41
|
+
if (args.length === 0) {
|
|
42
|
+
err('Usage: openclawmp unstar <assetRef>');
|
|
43
|
+
console.log(' Example: openclawmp unstar trigger/@xiaoyue/pdf-watcher');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!auth.isAuthenticated()) {
|
|
48
|
+
err('Authentication required. Run: openclawmp login');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const asset = await api.resolveAssetRef(args[0]);
|
|
53
|
+
const displayName = asset.displayName || asset.name || args[0];
|
|
54
|
+
|
|
55
|
+
const { status, data } = await api.del(`/api/assets/${asset.id}/star`);
|
|
56
|
+
|
|
57
|
+
if (status >= 200 && status < 300) {
|
|
58
|
+
ok(`☆ 已取消收藏 ${c('bold', displayName)}`);
|
|
59
|
+
} else if (status === 404) {
|
|
60
|
+
ok(`☆ 你还没有收藏 ${c('bold', displayName)}`);
|
|
61
|
+
} else {
|
|
62
|
+
err(`取消收藏失败 (${status}): ${data.error || data.message || JSON.stringify(data)}`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { runStar, runUnstar };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// commands/unbind.js — Unbind a device from your account
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const api = require('../api.js');
|
|
8
|
+
const config = require('../config.js');
|
|
9
|
+
const { fish, ok, err, warn, c, detail } = require('../ui.js');
|
|
10
|
+
|
|
11
|
+
async function run(args, flags) {
|
|
12
|
+
const deviceId = args[0] || config.getDeviceId();
|
|
13
|
+
|
|
14
|
+
if (!deviceId) {
|
|
15
|
+
err('No device ID specified and none found locally.');
|
|
16
|
+
console.log('');
|
|
17
|
+
console.log(` Usage: openclawmp unbind [deviceId]`);
|
|
18
|
+
console.log(` Without arguments, unbinds the current device.`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
fish(`Unbinding device ${c('dim', deviceId.slice(0, 12) + '...')} ...`);
|
|
23
|
+
|
|
24
|
+
const { status, data } = await api.del('/api/auth/device', { deviceId });
|
|
25
|
+
|
|
26
|
+
if (status >= 200 && status < 300 && data?.success) {
|
|
27
|
+
console.log('');
|
|
28
|
+
ok('设备已解绑');
|
|
29
|
+
detail('Device', deviceId.slice(0, 16) + '...');
|
|
30
|
+
console.log('');
|
|
31
|
+
} else {
|
|
32
|
+
err(data?.message || data?.error || `Unbind failed (HTTP ${status})`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { run };
|
package/lib/config.js
CHANGED
|
@@ -26,9 +26,9 @@ const DEVICE_JSON = path.join(OPENCLAW_STATE_DIR, 'identity', 'device.json');
|
|
|
26
26
|
// Valid asset types and their install subdirectories
|
|
27
27
|
const ASSET_TYPES = {
|
|
28
28
|
skill: 'skills',
|
|
29
|
-
plugin: '
|
|
29
|
+
plugin: 'extensions',
|
|
30
30
|
trigger: 'triggers',
|
|
31
|
-
channel: '
|
|
31
|
+
channel: 'extensions',
|
|
32
32
|
experience: 'experiences',
|
|
33
33
|
};
|
|
34
34
|
|
package/lib/help.js
CHANGED
|
@@ -23,6 +23,19 @@ function printHelp() {
|
|
|
23
23
|
console.log(' publish [path] Publish an asset to the market');
|
|
24
24
|
console.log(' login Show device authorization info');
|
|
25
25
|
console.log(' whoami Show current user / device info');
|
|
26
|
+
console.log('');
|
|
27
|
+
console.log(' Community:');
|
|
28
|
+
console.log(' star <assetRef> Star (收藏) an asset');
|
|
29
|
+
console.log(' unstar <assetRef> Remove star from an asset');
|
|
30
|
+
console.log(' comment <assetRef> <content> Post a comment (--rating 1-5, --as-agent)');
|
|
31
|
+
console.log(' comments <assetRef> View comments on an asset');
|
|
32
|
+
console.log(' issue <assetRef> <title> Create an issue (--body, --labels, --as-agent)');
|
|
33
|
+
console.log(' issues <assetRef> List issues on an asset');
|
|
34
|
+
console.log('');
|
|
35
|
+
console.log(' Account:');
|
|
36
|
+
console.log(' unbind [deviceId] Unbind a device (default: current device)');
|
|
37
|
+
console.log(' delete-account --confirm Delete account (unbind all + revoke keys)');
|
|
38
|
+
console.log('');
|
|
26
39
|
console.log(' help Show this help');
|
|
27
40
|
console.log('');
|
|
28
41
|
console.log(' Global options:');
|
|
@@ -37,6 +50,9 @@ function printHelp() {
|
|
|
37
50
|
console.log(' openclawmp install skill/@cybernova/web-search');
|
|
38
51
|
console.log(' openclawmp search "文件监控"');
|
|
39
52
|
console.log(' openclawmp list');
|
|
53
|
+
console.log(' openclawmp star trigger/@xiaoyue/pdf-watcher');
|
|
54
|
+
console.log(' openclawmp comment trigger/@xiaoyue/pdf-watcher "好用!" --rating 5');
|
|
55
|
+
console.log(' openclawmp issues tr-fc617094de29f938');
|
|
40
56
|
console.log('');
|
|
41
57
|
console.log(` Environment: ${c('dim', 'OPENCLAWMP_API — override API base URL')}`);
|
|
42
58
|
console.log('');
|