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.
- package/LICENSE +21 -0
- package/README.md +179 -0
- package/bin/openclawmp.js +77 -0
- package/lib/api.js +162 -0
- package/lib/auth.js +42 -0
- package/lib/cli-parser.js +61 -0
- package/lib/commands/info.js +56 -0
- package/lib/commands/install.js +270 -0
- package/lib/commands/list.js +36 -0
- package/lib/commands/login.js +37 -0
- package/lib/commands/publish.js +298 -0
- package/lib/commands/search.js +49 -0
- package/lib/commands/uninstall.js +54 -0
- package/lib/commands/whoami.js +48 -0
- package/lib/config.js +167 -0
- package/lib/help.js +45 -0
- package/lib/ui.js +103 -0
- package/package.json +30 -0
|
@@ -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 };
|