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 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: () => 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
- help: () => ({ run: () => printHelp() }),
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 };
@@ -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 'template':
167
- console.log(` To scaffold: ${c('bold', `openclawmp apply template/${slug} --workspace ./my-agent`)}`);
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
- // Fallback: generate from metadata if no package
217
+ // No package error (no fallback generation)
245
218
  if (!hasPackage) {
246
- info('No package available, generating from metadata...');
247
- generateSkillMd(asset, targetDir);
248
- console.log(' Generated: SKILL.md from metadata');
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 };
@@ -186,11 +186,57 @@ async function run(args, flags) {
186
186
  }
187
187
  }
188
188
 
189
- // Warn about missing description
190
- if (!meta.description) {
191
- warn('No description found — metadata may be incomplete');
192
- console.log(' The server will attempt to extract from package contents.');
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: 'plugins',
29
+ plugin: 'extensions',
30
30
  trigger: 'triggers',
31
- channel: 'channels',
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('');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclawmp",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "\ud83d\udc1f OpenClaw Marketplace CLI \u2014 \u6c34\u4ea7\u5e02\u573a\u547d\u4ee4\u884c\u5de5\u5177",
5
5
  "bin": {
6
6
  "openclawmp": "./bin/openclawmp.js"