openclawmp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,270 @@
1
+ // ============================================================================
2
+ // commands/install.js — Install an asset from the marketplace
3
+ // ============================================================================
4
+
5
+ 'use strict';
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const { execSync } = require('child_process');
10
+ const api = require('../api.js');
11
+ const config = require('../config.js');
12
+ const { fish, info, ok, warn, err, c, detail } = require('../ui.js');
13
+
14
+ /**
15
+ * Parse an install spec: type/@author/slug or type/slug
16
+ * @returns {{ type: string, slug: string, authorFilter: string }}
17
+ */
18
+ function parseSpec(spec) {
19
+ if (!spec || !spec.includes('/')) {
20
+ throw new Error('Format must be <type>/@<author>/<slug>, e.g. trigger/@xiaoyue/fs-event-trigger');
21
+ }
22
+
23
+ const firstSlash = spec.indexOf('/');
24
+ const type = spec.slice(0, firstSlash);
25
+ const rest = spec.slice(firstSlash + 1);
26
+
27
+ let slug, authorFilter = '';
28
+
29
+ if (rest.startsWith('@')) {
30
+ // Scoped: @author/slug
31
+ const slashIdx = rest.indexOf('/');
32
+ if (slashIdx === -1) {
33
+ throw new Error('Format must be <type>/@<author>/<slug>');
34
+ }
35
+ authorFilter = rest.slice(1, slashIdx); // strip @
36
+ slug = rest.slice(slashIdx + 1);
37
+ } else {
38
+ // Legacy: type/slug
39
+ slug = rest;
40
+ }
41
+
42
+ return { type, slug, authorFilter };
43
+ }
44
+
45
+ /**
46
+ * Extract a tar.gz or zip buffer to a directory
47
+ */
48
+ function extractPackage(buffer, targetDir) {
49
+ const tmpFile = path.join(require('os').tmpdir(), `openclawmp-pkg-${process.pid}-${Date.now()}`);
50
+ fs.writeFileSync(tmpFile, buffer);
51
+
52
+ try {
53
+ // Try tar first
54
+ try {
55
+ execSync(`tar xzf "${tmpFile}" -C "${targetDir}" --strip-components=1 2>/dev/null`, { stdio: 'pipe' });
56
+ return true;
57
+ } catch {
58
+ // Try without --strip-components
59
+ try {
60
+ execSync(`tar xzf "${tmpFile}" -C "${targetDir}" 2>/dev/null`, { stdio: 'pipe' });
61
+ return true;
62
+ } catch {
63
+ // Try unzip
64
+ try {
65
+ execSync(`unzip -o -q "${tmpFile}" -d "${targetDir}" 2>/dev/null`, { stdio: 'pipe' });
66
+ // If single subdirectory, move contents up
67
+ const entries = fs.readdirSync(targetDir);
68
+ const dirs = entries.filter(e => fs.statSync(path.join(targetDir, e)).isDirectory());
69
+ if (dirs.length === 1 && entries.length === 1) {
70
+ const subdir = path.join(targetDir, dirs[0]);
71
+ for (const f of fs.readdirSync(subdir)) {
72
+ fs.renameSync(path.join(subdir, f), path.join(targetDir, f));
73
+ }
74
+ fs.rmdirSync(subdir);
75
+ }
76
+ return true;
77
+ } catch {
78
+ return false;
79
+ }
80
+ }
81
+ }
82
+ } finally {
83
+ try { fs.unlinkSync(tmpFile); } catch {}
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Count files recursively in a directory
89
+ */
90
+ function countFiles(dir) {
91
+ let count = 0;
92
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
93
+ if (entry.isFile()) count++;
94
+ else if (entry.isDirectory()) count += countFiles(path.join(dir, entry.name));
95
+ }
96
+ return count;
97
+ }
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
+ /**
125
+ * Write manifest.json for the installed asset
126
+ */
127
+ function writeManifest(asset, targetDir, hasPackage) {
128
+ const manifest = {
129
+ schema: 1,
130
+ type: asset.type,
131
+ name: asset.name,
132
+ displayName: asset.displayName || '',
133
+ version: asset.version,
134
+ author: asset.author,
135
+ description: asset.description || '',
136
+ tags: asset.tags || [],
137
+ category: asset.category || '',
138
+ installedFrom: 'openclawmp',
139
+ registryId: asset.id,
140
+ hasPackage,
141
+ };
142
+ fs.writeFileSync(path.join(targetDir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
143
+ }
144
+
145
+ /**
146
+ * Post-install hints per asset type
147
+ */
148
+ function showPostInstallHints(type, slug) {
149
+ switch (type) {
150
+ case 'skill':
151
+ console.log(` ${c('green', 'Ready!')} Will be loaded in the next agent session.`);
152
+ 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
+ case 'plugin':
161
+ console.log(` ${c('yellow', 'Requires restart:')} openclaw gateway restart`);
162
+ break;
163
+ case 'channel':
164
+ console.log(` ${c('yellow', 'Requires config:')} Set credentials in openclaw.json, then restart`);
165
+ break;
166
+ case 'template':
167
+ console.log(` To scaffold: ${c('bold', `openclawmp apply template/${slug} --workspace ./my-agent`)}`);
168
+ break;
169
+ }
170
+ }
171
+
172
+ async function run(args, flags) {
173
+ if (args.length === 0) {
174
+ err('Usage: openclawmp install <type>/@<author>/<slug>');
175
+ console.log(' Example: openclawmp install trigger/@xiaoyue/fs-event-trigger');
176
+ process.exit(1);
177
+ }
178
+
179
+ const { type, slug, authorFilter } = parseSpec(args[0]);
180
+ const displaySpec = `${type}/${authorFilter ? `@${authorFilter}/` : ''}${slug}`;
181
+
182
+ fish('OpenClaw Marketplace Install');
183
+ console.log('');
184
+ info(`Looking up ${c('bold', displaySpec)} in the market...`);
185
+
186
+ // Query the registry
187
+ const asset = await api.findAsset(type, slug, authorFilter);
188
+ if (!asset) {
189
+ err(`Asset ${c('bold', displaySpec)} not found in the market.`);
190
+ console.log('');
191
+ console.log(` Try: openclawmp search ${slug}`);
192
+ process.exit(1);
193
+ }
194
+
195
+ const displayName = asset.displayName || asset.name;
196
+ const version = asset.version;
197
+ const authorName = asset.author?.name || 'unknown';
198
+ const authorId = asset.author?.id || '';
199
+
200
+ console.log(` ${c('bold', displayName)} ${c('dim', `v${version}`)}`);
201
+ console.log(` by ${c('cyan', authorName)} ${c('dim', `(${authorId})`)}`);
202
+ console.log('');
203
+
204
+ // Determine install directory
205
+ const targetDir = path.join(config.installDirForType(type), slug);
206
+
207
+ // Check if already installed
208
+ if (fs.existsSync(targetDir)) {
209
+ if (flags.force || flags.y) {
210
+ fs.rmSync(targetDir, { recursive: true, force: true });
211
+ } else {
212
+ warn(`Already installed at ${targetDir}`);
213
+ // In non-interactive mode, skip confirmation
214
+ const readline = require('readline');
215
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
216
+ const answer = await new Promise(resolve => {
217
+ rl.question(' Overwrite? [y/N] ', resolve);
218
+ });
219
+ rl.close();
220
+ if (!/^[yY]/.test(answer)) {
221
+ console.log(' Aborted.');
222
+ return;
223
+ }
224
+ fs.rmSync(targetDir, { recursive: true, force: true });
225
+ }
226
+ }
227
+
228
+ info(`Installing to ${c('dim', targetDir)}...`);
229
+ fs.mkdirSync(targetDir, { recursive: true });
230
+
231
+ // Try downloading the actual package
232
+ let hasPackage = false;
233
+ const pkgBuffer = await api.download(`/api/assets/${asset.id}/download`);
234
+
235
+ if (pkgBuffer && pkgBuffer.length > 0) {
236
+ info('📦 Downloading package from registry...');
237
+ hasPackage = extractPackage(pkgBuffer, targetDir);
238
+ if (hasPackage) {
239
+ const fileCount = countFiles(targetDir);
240
+ console.log(` 📦 Extracted ${c('bold', String(fileCount))} files from package`);
241
+ }
242
+ }
243
+
244
+ // Fallback: generate from metadata if no package
245
+ if (!hasPackage) {
246
+ info('No package available, generating from metadata...');
247
+ generateSkillMd(asset, targetDir);
248
+ console.log(' Generated: SKILL.md from metadata');
249
+ }
250
+
251
+ // Always write manifest.json
252
+ writeManifest(asset, targetDir, hasPackage);
253
+ console.log(' Created: manifest.json');
254
+
255
+ // Update lockfile
256
+ const lockKey = `${type}/${authorId ? `@${authorId}/` : ''}${slug}`;
257
+ config.updateLockfile(lockKey, version, targetDir);
258
+
259
+ console.log('');
260
+ ok(`Installed ${c('bold', displayName)} v${version}`);
261
+ detail('Location', targetDir);
262
+ detail('Registry', `${config.getApiBase()}/asset/${asset.id}`);
263
+ detail('Command', `openclawmp install ${type}/@${authorId}/${slug}`);
264
+ console.log('');
265
+
266
+ showPostInstallHints(type, slug);
267
+ console.log('');
268
+ }
269
+
270
+ module.exports = { run };
@@ -0,0 +1,36 @@
1
+ // ============================================================================
2
+ // commands/list.js — List installed assets
3
+ // ============================================================================
4
+
5
+ 'use strict';
6
+
7
+ const config = require('../config.js');
8
+ const { fish, c } = require('../ui.js');
9
+
10
+ async function run() {
11
+ fish('Installed from OpenClaw Marketplace');
12
+ console.log('');
13
+
14
+ const lock = config.readLockfile();
15
+ const installed = lock.installed || {};
16
+ const keys = Object.keys(installed).sort();
17
+
18
+ if (keys.length === 0) {
19
+ console.log(' Nothing installed yet.');
20
+ console.log(' Try: openclawmp search web-search');
21
+ return;
22
+ }
23
+
24
+ for (const key of keys) {
25
+ const entry = installed[key];
26
+ const ver = entry.version || '?';
27
+ const loc = entry.location || '?';
28
+ const ts = (entry.installedAt || '?').slice(0, 10);
29
+
30
+ console.log(` 📦 ${c('bold', key)} v${ver} ${c('dim', `(${ts})`)}`);
31
+ console.log(` ${c('dim', loc)}`);
32
+ console.log('');
33
+ }
34
+ }
35
+
36
+ module.exports = { run };
@@ -0,0 +1,37 @@
1
+ // ============================================================================
2
+ // commands/login.js — Device authorization (login / authorize)
3
+ // ============================================================================
4
+
5
+ 'use strict';
6
+
7
+ const config = require('../config.js');
8
+ const { fish, err, c, detail } = require('../ui.js');
9
+
10
+ async function run() {
11
+ const deviceId = config.getDeviceId();
12
+
13
+ console.log('');
14
+ fish('Device Authorization');
15
+ console.log('');
16
+
17
+ if (!deviceId) {
18
+ err(`No OpenClaw device identity found at ${config.DEVICE_JSON}`);
19
+ console.log('');
20
+ console.log(' Make sure OpenClaw is installed and has been started at least once.');
21
+ process.exit(1);
22
+ }
23
+
24
+ console.log(` Device ID: ${deviceId.slice(0, 16)}...`);
25
+ console.log('');
26
+ console.log(' To authorize this device, you need:');
27
+ console.log(` 1. An account on ${c('bold', config.getApiBase())} (GitHub/Google login)`);
28
+ console.log(' 2. An activated invite code');
29
+ console.log('');
30
+ console.log(' Then authorize via the web UI, or ask your Agent to call:');
31
+ console.log(` POST /api/auth/device { "deviceId": "${deviceId}" }`);
32
+ console.log('');
33
+ console.log(` Once authorized, you can publish with: ${c('bold', 'openclawmp publish ./')}`);
34
+ console.log('');
35
+ }
36
+
37
+ module.exports = { run };
@@ -0,0 +1,298 @@
1
+ // ============================================================================
2
+ // commands/publish.js — Publish a local asset directory to the marketplace
3
+ // ============================================================================
4
+
5
+ 'use strict';
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const { execSync } = require('child_process');
10
+ const api = require('../api.js');
11
+ const config = require('../config.js');
12
+ const { fish, info, ok, warn, err, c, detail } = require('../ui.js');
13
+
14
+ /**
15
+ * Parse SKILL.md frontmatter (YAML-like key: value pairs between --- markers)
16
+ */
17
+ function parseFrontmatter(content) {
18
+ const fm = {};
19
+ let body = content;
20
+
21
+ const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)/);
22
+ if (match) {
23
+ const fmText = match[1];
24
+ body = match[2].trim();
25
+
26
+ for (const line of fmText.split('\n')) {
27
+ const trimmed = line.trim();
28
+ if (!trimmed || trimmed.startsWith('#')) continue;
29
+ const kv = trimmed.match(/^([\w-]+)\s*:\s*(.*)/);
30
+ if (kv) {
31
+ let val = kv[2].trim();
32
+ // Strip surrounding quotes
33
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
34
+ val = val.slice(1, -1);
35
+ }
36
+ fm[kv[1]] = val;
37
+ }
38
+ }
39
+ }
40
+
41
+ return { frontmatter: fm, body };
42
+ }
43
+
44
+ /**
45
+ * Extract metadata from a skill directory
46
+ */
47
+ function extractMetadata(skillDir) {
48
+ const hasSkillMd = fs.existsSync(path.join(skillDir, 'SKILL.md'));
49
+ const hasPluginJson = fs.existsSync(path.join(skillDir, 'openclaw.plugin.json'));
50
+ const hasPackageJson = fs.existsSync(path.join(skillDir, 'package.json'));
51
+ const hasReadme = fs.existsSync(path.join(skillDir, 'README.md'));
52
+
53
+ let name = '', displayName = '', description = '', version = '1.0.0';
54
+ let readme = '', tags = [], category = '', longDescription = '';
55
+ let detectedType = '';
56
+
57
+ // --- Priority 1: SKILL.md ---
58
+ if (hasSkillMd) {
59
+ const content = fs.readFileSync(path.join(skillDir, 'SKILL.md'), 'utf-8');
60
+ const { frontmatter: fm, body } = parseFrontmatter(content);
61
+
62
+ name = fm.name || '';
63
+ displayName = fm.displayName || fm['display-name'] || '';
64
+ description = fm.description || '';
65
+ version = fm.version || '1.0.0';
66
+ readme = body;
67
+ if (fm.tags) {
68
+ tags = fm.tags.split(',').map(t => t.trim()).filter(Boolean);
69
+ }
70
+ category = fm.category || '';
71
+ longDescription = fm.longDescription || description;
72
+ detectedType = fm.type || 'skill';
73
+ }
74
+
75
+ // --- Priority 2: openclaw.plugin.json ---
76
+ if (hasPluginJson && !name) {
77
+ try {
78
+ const plugin = JSON.parse(fs.readFileSync(path.join(skillDir, 'openclaw.plugin.json'), 'utf-8'));
79
+ name = name || plugin.id || '';
80
+ displayName = displayName || plugin.name || '';
81
+ description = description || plugin.description || '';
82
+ version = version === '1.0.0' ? (plugin.version || '1.0.0') : version;
83
+
84
+ // Detect channel type
85
+ if (Array.isArray(plugin.channels) && plugin.channels.length > 0) {
86
+ detectedType = detectedType || 'channel';
87
+ } else {
88
+ detectedType = detectedType || 'plugin';
89
+ }
90
+ } catch {}
91
+ }
92
+
93
+ // --- Priority 3: package.json ---
94
+ if (hasPackageJson && !name) {
95
+ try {
96
+ const pkg = JSON.parse(fs.readFileSync(path.join(skillDir, 'package.json'), 'utf-8'));
97
+ let pkgName = pkg.name || '';
98
+ // Strip @scope/ prefix
99
+ if (pkgName.startsWith('@') && pkgName.includes('/')) {
100
+ pkgName = pkgName.split('/').pop();
101
+ }
102
+ name = name || pkgName;
103
+ displayName = displayName || pkgName;
104
+ description = description || pkg.description || '';
105
+ version = version === '1.0.0' ? (pkg.version || '1.0.0') : version;
106
+ } catch {}
107
+ }
108
+
109
+ // --- Priority 4: README.md ---
110
+ if (hasReadme) {
111
+ try {
112
+ const readmeContent = fs.readFileSync(path.join(skillDir, 'README.md'), 'utf-8');
113
+ if (!readme) readme = readmeContent;
114
+ if (!displayName) {
115
+ const titleMatch = readmeContent.match(/^#\s+(.+)$/m);
116
+ if (titleMatch) displayName = titleMatch[1].trim();
117
+ }
118
+ if (!description) {
119
+ for (const line of readmeContent.split('\n')) {
120
+ const t = line.trim();
121
+ if (!t || t.startsWith('#') || t.startsWith('---')) continue;
122
+ description = t;
123
+ break;
124
+ }
125
+ }
126
+ } catch {}
127
+ }
128
+
129
+ // Fallbacks
130
+ if (!name) name = path.basename(skillDir);
131
+ if (!displayName) displayName = name;
132
+ if (!detectedType) detectedType = '';
133
+
134
+ return {
135
+ name, displayName, type: detectedType,
136
+ description, version, readme,
137
+ tags, category,
138
+ longDescription: longDescription || description,
139
+ };
140
+ }
141
+
142
+ async function run(args, flags) {
143
+ let skillDir = args[0] || '.';
144
+ skillDir = path.resolve(skillDir);
145
+
146
+ fish(`Publishing from ${c('bold', skillDir)}`);
147
+ console.log('');
148
+
149
+ // Check device ID
150
+ const deviceId = config.getDeviceId();
151
+ if (!deviceId) {
152
+ err('No OpenClaw device identity found.');
153
+ console.log('');
154
+ console.log(` Expected: ${config.DEVICE_JSON}`);
155
+ console.log(' Make sure OpenClaw is installed and has been started at least once.');
156
+ console.log('');
157
+ console.log(` Your device must be authorized first:`);
158
+ console.log(` 1. Login on ${c('bold', config.getApiBase())} (GitHub/Google)`);
159
+ console.log(' 2. Activate an invite code');
160
+ console.log(' 3. Authorize this device (your deviceId will be auto-detected)');
161
+ process.exit(1);
162
+ }
163
+
164
+ info(`Device ID: ${deviceId.slice(0, 12)}...`);
165
+
166
+ // Extract metadata
167
+ const meta = extractMetadata(skillDir);
168
+
169
+ // If type not detected, prompt user
170
+ if (!meta.type) {
171
+ warn('Could not auto-detect asset type (no SKILL.md or openclaw.plugin.json found)');
172
+ console.log(' Available types: skill, plugin, channel, trigger, experience');
173
+
174
+ const readline = require('readline');
175
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
176
+ meta.type = await new Promise(resolve => {
177
+ rl.question(' Enter asset type: ', answer => {
178
+ rl.close();
179
+ resolve(answer.trim());
180
+ });
181
+ });
182
+
183
+ if (!meta.type) {
184
+ err('Asset type is required');
185
+ process.exit(1);
186
+ }
187
+ }
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.');
193
+ console.log('');
194
+ }
195
+
196
+ // Show preview
197
+ console.log('');
198
+ console.log(` Name: ${meta.name}`);
199
+ console.log(` Display: ${meta.displayName}`);
200
+ console.log(` Type: ${meta.type}`);
201
+ console.log(` Version: ${meta.version}`);
202
+ console.log(` Description: ${(meta.description || '').slice(0, 80)}`);
203
+ if (meta.tags.length) {
204
+ console.log(` Tags: ${meta.tags.join(', ')}`);
205
+ }
206
+ console.log('');
207
+
208
+ // Confirm
209
+ if (!flags.yes && !flags.y) {
210
+ const readline = require('readline');
211
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
212
+ const answer = await new Promise(resolve => {
213
+ rl.question(` Publish to ${config.getApiBase()}? [Y/n] `, resolve);
214
+ });
215
+ rl.close();
216
+ if (/^[nN]/.test(answer)) {
217
+ info('Cancelled.');
218
+ return;
219
+ }
220
+ }
221
+
222
+ // Create tarball
223
+ const tarball = path.join(require('os').tmpdir(), `openclawmp-publish-${Date.now()}.tar.gz`);
224
+ try {
225
+ execSync(`tar czf "${tarball}" -C "${skillDir}" .`, { stdio: 'pipe' });
226
+ } catch (e) {
227
+ err('Failed to create package tarball');
228
+ process.exit(1);
229
+ }
230
+
231
+ const tarStats = fs.statSync(tarball);
232
+ const sizeKb = (tarStats.size / 1024).toFixed(1);
233
+ info(`Package: ${sizeKb}KB compressed`);
234
+
235
+ // Build payload
236
+ const payload = {
237
+ name: meta.name,
238
+ displayName: meta.displayName,
239
+ type: meta.type,
240
+ description: meta.description,
241
+ version: meta.version,
242
+ readme: meta.readme,
243
+ tags: meta.tags,
244
+ category: meta.category,
245
+ longDescription: meta.longDescription,
246
+ authorId: process.env.SEAFOOD_AUTHOR_ID || '',
247
+ authorName: process.env.SEAFOOD_AUTHOR_NAME || '',
248
+ authorAvatar: process.env.SEAFOOD_AUTHOR_AVATAR || '',
249
+ };
250
+
251
+ // POST multipart: metadata + package file
252
+ const { FormData, File } = require('node:buffer');
253
+ let formData;
254
+
255
+ // Node 18+ has global FormData via undici
256
+ if (typeof globalThis.FormData !== 'undefined') {
257
+ formData = new globalThis.FormData();
258
+ formData.append('metadata', new Blob([JSON.stringify(payload)], { type: 'application/json' }), 'metadata.json');
259
+ const tarBuffer = fs.readFileSync(tarball);
260
+ formData.append('package', new Blob([tarBuffer], { type: 'application/gzip' }), 'package.tar.gz');
261
+ } else {
262
+ // Fallback for older Node — use raw fetch with multipart boundary
263
+ err('FormData not available. Requires Node.js 18+ with fetch support.');
264
+ process.exit(1);
265
+ }
266
+
267
+ const { status, data: respData } = await api.postMultipart('/api/v1/assets/publish', formData);
268
+
269
+ // Clean up tarball
270
+ try { fs.unlinkSync(tarball); } catch {}
271
+
272
+ if (status === 200 || status === 201) {
273
+ const assetId = respData?.data?.id || 'unknown';
274
+ const fileCount = respData?.data?.files?.length || '?';
275
+
276
+ console.log('');
277
+ ok('Published successfully! 🎉');
278
+ console.log('');
279
+ detail('ID', assetId);
280
+ detail('Files', fileCount);
281
+ detail('Page', `${config.getApiBase()}/asset/${assetId}`);
282
+ console.log('');
283
+
284
+ // Check for metadataIncomplete flag
285
+ if (respData?.data?.metadataIncomplete) {
286
+ const missingFields = (respData.data.missingFields || []).join(', ');
287
+ warn(`部分元数据缺失: ${missingFields}`);
288
+ console.log(' 建议让 Agent 自动补全,或手动编辑后重新发布');
289
+ console.log('');
290
+ }
291
+ } else {
292
+ const errorMsg = respData?.error || JSON.stringify(respData) || 'Unknown error';
293
+ err(`Publish failed (HTTP ${status}): ${errorMsg}`);
294
+ process.exit(1);
295
+ }
296
+ }
297
+
298
+ module.exports = { run };
@@ -0,0 +1,49 @@
1
+ // ============================================================================
2
+ // commands/search.js — Search the marketplace
3
+ // ============================================================================
4
+
5
+ 'use strict';
6
+
7
+ const api = require('../api.js');
8
+ const { fish, err, c, typeIcon } = require('../ui.js');
9
+
10
+ async function run(args) {
11
+ const query = args.join(' ');
12
+ if (!query) {
13
+ err('Usage: openclawmp search <query>');
14
+ process.exit(1);
15
+ }
16
+
17
+ fish(`Searching the market for "${query}"...`);
18
+ console.log('');
19
+
20
+ const result = await api.searchAssets(query);
21
+ const assets = result?.data?.assets || [];
22
+ const total = result?.data?.total || 0;
23
+
24
+ if (assets.length === 0) {
25
+ console.log(' No results found.');
26
+ return;
27
+ }
28
+
29
+ console.log(` Found ${total} result(s):`);
30
+ console.log('');
31
+
32
+ for (const a of assets) {
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';
37
+
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}`);
40
+
41
+ const desc = (a.description || '').slice(0, 80);
42
+ if (desc) {
43
+ console.log(` ${c('dim', desc)}`);
44
+ }
45
+ console.log('');
46
+ }
47
+ }
48
+
49
+ module.exports = { run };