openclawmp 0.1.1 → 0.1.3

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
@@ -1,6 +1,7 @@
1
1
  // ============================================================================
2
2
  // api.js — HTTP request helpers for the OpenClaw Marketplace API
3
3
  //
4
+ // Uses V1 API endpoints for lightweight responses (AssetCompact).
4
5
  // Uses Node.js built-in fetch (available since Node 18)
5
6
  // ============================================================================
6
7
 
@@ -22,7 +23,7 @@ function authHeaders() {
22
23
 
23
24
  /**
24
25
  * Make a GET request to the API
25
- * @param {string} apiPath - API path (e.g., '/api/assets')
26
+ * @param {string} apiPath - API path (e.g., '/api/v1/assets')
26
27
  * @param {object} [params] - Query parameters
27
28
  * @returns {Promise<object>} Parsed JSON response
28
29
  */
@@ -107,40 +108,53 @@ async function download(apiPath) {
107
108
  }
108
109
 
109
110
  /**
110
- * Search assets
111
+ * Search assets via V1 API (returns lightweight AssetCompact items).
112
+ *
113
+ * V1 response shape: { query, total, items: AssetCompact[], nextCursor }
114
+ * AssetCompact fields: id, name, displayName, type, description, tags,
115
+ * installs, rating, author (string), authorId, version, installCommand,
116
+ * updatedAt, category
117
+ *
111
118
  * @param {string} query
112
119
  * @param {object} [opts] - { type, limit }
113
- * @returns {Promise<object>}
120
+ * @returns {Promise<object>} V1 search response
114
121
  */
115
122
  async function searchAssets(query, opts = {}) {
116
123
  const params = { q: query, limit: opts.limit || 20 };
117
124
  if (opts.type) params.type = opts.type;
118
- return get('/api/assets', params);
125
+ return get('/api/v1/search', params);
119
126
  }
120
127
 
121
128
  /**
122
- * Find an asset by type and slug (with optional author filter)
129
+ * Find an asset by type and slug (with optional author filter).
130
+ * Uses V1 list endpoint for lightweight data.
131
+ *
123
132
  * @param {string} type
124
133
  * @param {string} slug
125
- * @param {string} [authorFilter]
134
+ * @param {string} [authorFilter] - author ID or author name to filter by
126
135
  * @returns {Promise<object|null>}
127
136
  */
128
137
  async function findAsset(type, slug, authorFilter) {
129
- const result = await get('/api/assets', { q: slug, limit: 50 });
130
- const assets = result?.data?.assets || [];
138
+ const result = await get('/api/v1/assets', { q: slug, type, limit: 50 });
139
+ const assets = result?.items || [];
131
140
 
132
- // Exact match on type + name
133
- let matches = assets.filter(a => a.type === type && a.name === slug);
141
+ // Exact match on name
142
+ let matches = assets.filter(a => a.name === slug);
134
143
  if (authorFilter) {
135
- const authorMatches = matches.filter(a => (a.author?.id || '') === authorFilter);
144
+ // authorFilter could be an authorId or author name
145
+ const authorMatches = matches.filter(a =>
146
+ a.authorId === authorFilter || a.author === authorFilter
147
+ );
136
148
  if (authorMatches.length > 0) matches = authorMatches;
137
149
  }
138
150
 
139
- // Fallback: partial match
151
+ // Fallback: partial match on name
140
152
  if (matches.length === 0) {
141
- matches = assets.filter(a => a.type === type && a.name.includes(slug));
153
+ matches = assets.filter(a => a.name.includes(slug));
142
154
  if (authorFilter) {
143
- const authorMatches = matches.filter(a => (a.author?.id || '') === authorFilter);
155
+ const authorMatches = matches.filter(a =>
156
+ a.authorId === authorFilter || a.author === authorFilter
157
+ );
144
158
  if (authorMatches.length > 0) matches = authorMatches;
145
159
  }
146
160
  }
@@ -148,15 +162,100 @@ async function findAsset(type, slug, authorFilter) {
148
162
  if (matches.length === 0) return null;
149
163
 
150
164
  // Prefer the one with an author ID
151
- matches.sort((a, b) => (b.author?.id || '').localeCompare(a.author?.id || ''));
165
+ matches.sort((a, b) => (b.authorId || '').localeCompare(a.authorId || ''));
152
166
  return matches[0];
153
167
  }
154
168
 
169
+ /**
170
+ * Get asset detail (L2) by ID via V1 API.
171
+ * Returns full detail including readme, files, versions.
172
+ *
173
+ * @param {string} id - Asset ID
174
+ * @returns {Promise<object|null>}
175
+ */
176
+ async function getAssetById(id) {
177
+ try {
178
+ return await get(`/api/v1/assets/${id}`);
179
+ } catch (e) {
180
+ if (e.message.includes('404')) return null;
181
+ throw e;
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Make a DELETE request to the API
187
+ * @param {string} apiPath
188
+ * @param {object} [body] - Optional JSON body
189
+ * @returns {Promise<{status: number, data: object}>}
190
+ */
191
+ async function del(apiPath, body) {
192
+ const url = new URL(apiPath, config.getApiBase());
193
+
194
+ const opts = {
195
+ method: 'DELETE',
196
+ headers: { ...authHeaders() },
197
+ };
198
+ if (body) {
199
+ opts.headers['Content-Type'] = 'application/json';
200
+ opts.body = JSON.stringify(body);
201
+ }
202
+
203
+ const res = await fetch(url.toString(), opts);
204
+ const data = await res.json().catch(() => ({}));
205
+ return { status: res.status, data };
206
+ }
207
+
208
+ /**
209
+ * Resolve an asset reference to a full asset object.
210
+ * Accepts:
211
+ * - Direct ID: "s-abc123", "tr-fc617094de29f938"
212
+ * - type/@author/slug: "trigger/@xiaoyue/pdf-watcher"
213
+ *
214
+ * Uses V1 API: GET /api/v1/assets/:id for ID lookups,
215
+ * findAsset() (V1 search) for type/slug lookups.
216
+ *
217
+ * @param {string} ref
218
+ * @returns {Promise<object>} asset object with at least { id, name, ... }
219
+ */
220
+ async function resolveAssetRef(ref) {
221
+ // Direct ID pattern: prefix + dash + hex
222
+ if (/^[a-z]+-[0-9a-f]{8,}$/.test(ref)) {
223
+ const result = await getAssetById(ref);
224
+ if (!result) throw new Error(`Asset not found: ${ref}`);
225
+ return result;
226
+ }
227
+
228
+ // type/@author/slug format
229
+ const parts = ref.split('/');
230
+ if (parts.length < 2) {
231
+ throw new Error(`Invalid asset reference: ${ref}. Use <id> or <type>/@<author>/<slug>`);
232
+ }
233
+
234
+ const type = parts[0];
235
+ let slug, authorFilter = '';
236
+
237
+ if (parts.length >= 3 && parts[1].startsWith('@')) {
238
+ authorFilter = parts[1].slice(1);
239
+ slug = parts.slice(2).join('/');
240
+ } else {
241
+ slug = parts.slice(1).join('/');
242
+ }
243
+
244
+ const asset = await findAsset(type, slug, authorFilter);
245
+ if (!asset) {
246
+ throw new Error(`Asset not found: ${ref}`);
247
+ }
248
+ return asset;
249
+ }
250
+
155
251
  module.exports = {
156
252
  get,
157
253
  post,
254
+ del,
158
255
  postMultipart,
159
256
  download,
160
257
  searchAssets,
161
258
  findAsset,
259
+ getAssetById,
260
+ resolveAssetRef,
162
261
  };
@@ -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 };
@@ -27,8 +27,9 @@ async function run(args) {
27
27
  process.exit(1);
28
28
  }
29
29
 
30
- const authorName = asset.author?.name || 'unknown';
31
- const authorId = asset.author?.id || '';
30
+ // V1 AssetCompact: author is a string, authorId is separate
31
+ const authorName = asset.author || 'unknown';
32
+ const authorId = asset.authorId || '';
32
33
  const tags = (asset.tags || []).join(', ');
33
34
 
34
35
  console.log('');
@@ -38,8 +39,7 @@ async function run(args) {
38
39
  console.log(` Package: ${asset.name}`);
39
40
  console.log(` Version: ${asset.version}`);
40
41
  console.log(` Author: ${c('cyan', authorName)} ${c('dim', `(${authorId})`)}`);
41
- console.log(` Score: ${asset.hubScore || 0}`);
42
- console.log(` Downloads: ${asset.downloads || 0}`);
42
+ console.log(` Installs: ${asset.installs || 0}`);
43
43
  if (tags) {
44
44
  console.log(` Tags: ${tags}`);
45
45
  }
@@ -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
  */
@@ -131,7 +106,8 @@ function writeManifest(asset, targetDir, hasPackage) {
131
106
  name: asset.name,
132
107
  displayName: asset.displayName || '',
133
108
  version: asset.version,
134
- author: asset.author,
109
+ author: asset.author || '',
110
+ authorId: asset.authorId || '',
135
111
  description: asset.description || '',
136
112
  tags: asset.tags || [],
137
113
  category: asset.category || '',
@@ -145,26 +121,24 @@ function writeManifest(asset, targetDir, hasPackage) {
145
121
  /**
146
122
  * Post-install hints per asset type
147
123
  */
148
- function showPostInstallHints(type, slug) {
124
+ function showPostInstallHints(type, slug, targetDir) {
149
125
  switch (type) {
150
126
  case 'skill':
151
127
  console.log(` ${c('green', 'Ready!')} Will be loaded in the next agent session.`);
152
128
  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
129
  case 'plugin':
161
130
  console.log(` ${c('yellow', 'Requires restart:')} openclaw gateway restart`);
162
131
  break;
163
132
  case 'channel':
164
133
  console.log(` ${c('yellow', 'Requires config:')} Set credentials in openclaw.json, then restart`);
165
134
  break;
166
- case 'template':
167
- console.log(` To scaffold: ${c('bold', `openclawmp apply template/${slug} --workspace ./my-agent`)}`);
135
+ case 'trigger':
136
+ console.log(` ${c('yellow', 'Manual setup:')} Read README.md for cron/heartbeat configuration`);
137
+ console.log(` ${c('dim', `cat ${targetDir}/README.md`)}`);
138
+ break;
139
+ case 'experience':
140
+ console.log(` ${c('yellow', 'Reference:')} Read README.md for setup instructions`);
141
+ console.log(` ${c('dim', `cat ${targetDir}/README.md`)}`);
168
142
  break;
169
143
  }
170
144
  }
@@ -194,8 +168,8 @@ async function run(args, flags) {
194
168
 
195
169
  const displayName = asset.displayName || asset.name;
196
170
  const version = asset.version;
197
- const authorName = asset.author?.name || 'unknown';
198
- const authorId = asset.author?.id || '';
171
+ const authorName = asset.author || 'unknown';
172
+ const authorId = asset.authorId || '';
199
173
 
200
174
  console.log(` ${c('bold', displayName)} ${c('dim', `v${version}`)}`);
201
175
  console.log(` by ${c('cyan', authorName)} ${c('dim', `(${authorId})`)}`);
@@ -230,7 +204,7 @@ async function run(args, flags) {
230
204
 
231
205
  // Try downloading the actual package
232
206
  let hasPackage = false;
233
- const pkgBuffer = await api.download(`/api/assets/${asset.id}/download`);
207
+ const pkgBuffer = await api.download(`/api/v1/assets/${asset.id}/download`);
234
208
 
235
209
  if (pkgBuffer && pkgBuffer.length > 0) {
236
210
  info('📦 Downloading package from registry...');
@@ -241,11 +215,12 @@ async function run(args, flags) {
241
215
  }
242
216
  }
243
217
 
244
- // Fallback: generate from metadata if no package
218
+ // No package error (no fallback generation)
245
219
  if (!hasPackage) {
246
- info('No package available, generating from metadata...');
247
- generateSkillMd(asset, targetDir);
248
- console.log(' Generated: SKILL.md from metadata');
220
+ try { fs.rmSync(targetDir, { recursive: true, force: true }); } catch {}
221
+ err('该资产没有可安装的 package。');
222
+ console.log(` 请在水产市场查看详情:${config.getApiBase()}/asset/${asset.id}`);
223
+ process.exit(1);
249
224
  }
250
225
 
251
226
  // Always write manifest.json
@@ -263,7 +238,7 @@ async function run(args, flags) {
263
238
  detail('Command', `openclawmp install ${type}/@${authorId}/${slug}`);
264
239
  console.log('');
265
240
 
266
- showPostInstallHints(type, slug);
241
+ showPostInstallHints(type, slug, targetDir);
267
242
  console.log('');
268
243
  }
269
244
 
@@ -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
@@ -18,8 +18,8 @@ async function run(args) {
18
18
  console.log('');
19
19
 
20
20
  const result = await api.searchAssets(query);
21
- const assets = result?.data?.assets || [];
22
- const total = result?.data?.total || 0;
21
+ const assets = result?.items || [];
22
+ const total = result?.total || 0;
23
23
 
24
24
  if (assets.length === 0) {
25
25
  console.log(' No results found.');
@@ -31,12 +31,12 @@ async function run(args) {
31
31
 
32
32
  for (const a of assets) {
33
33
  const icon = typeIcon(a.type);
34
- const score = a.hubScore || 0;
35
- const author = a.author?.name || 'unknown';
36
- const authorId = a.author?.id || 'unknown';
34
+ const installs = a.installs || 0;
35
+ const author = a.author || 'unknown';
36
+ const authorId = a.authorId || 'unknown';
37
37
 
38
38
  console.log(` ${icon} ${c('bold', a.displayName)}`);
39
- console.log(` ${a.type}/@${authorId}/${a.name} • v${a.version} • by ${c('cyan', author)} • Score: ${score}`);
39
+ console.log(` ${a.type}/@${authorId}/${a.name} • v${a.version} • by ${c('cyan', author)} • Installs: ${installs}`);
40
40
 
41
41
  const desc = (a.description || '').slice(0, 80);
42
42
  if (desc) {
@@ -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.3",
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"