@xiashe/skill 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/README.md +27 -0
- package/bin/xiashe-skill.mjs +475 -0
- package/package.json +15 -0
package/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# @xiashe/skill
|
|
2
|
+
|
|
3
|
+
Creator-facing XiaShe Skill registry helper.
|
|
4
|
+
|
|
5
|
+
This package is intentionally separate from the full `@xiashe/cli` product CLI and from `@xiashe/agent-skill`, which is the runtime feed helper for external Agents.
|
|
6
|
+
|
|
7
|
+
## Purpose
|
|
8
|
+
|
|
9
|
+
`xiashe-skill` helps creators prepare a Skill folder for third-party Skill hubs:
|
|
10
|
+
|
|
11
|
+
- inspect a local Skill project
|
|
12
|
+
- write an explicit `xiashe.skill.json` registry manifest
|
|
13
|
+
- copy a clean package directory
|
|
14
|
+
- generate an upload prompt that the creator can hand to an Agent
|
|
15
|
+
|
|
16
|
+
It does not install background services, run postinstall hooks, read secrets, or upload to third-party hubs.
|
|
17
|
+
|
|
18
|
+
## Local development
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
node packages/xiashe-skill-cli/bin/xiashe-skill.mjs --help
|
|
22
|
+
node packages/xiashe-skill-cli/bin/xiashe-skill.mjs inspect .
|
|
23
|
+
node packages/xiashe-skill-cli/bin/xiashe-skill.mjs attach . --code XS-XXXX-XXXX
|
|
24
|
+
node packages/xiashe-skill-cli/bin/xiashe-skill.mjs attach . --public-token pub_xxx --skill-id sk_xxx
|
|
25
|
+
node packages/xiashe-skill-cli/bin/xiashe-skill.mjs pack . --out .xiashe/package
|
|
26
|
+
node packages/xiashe-skill-cli/bin/xiashe-skill.mjs prompt . --hub red --package-url https://example.com/my-skill.zip
|
|
27
|
+
```
|
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import { copyFile, mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
|
|
9
|
+
const VERSION = '0.1.0';
|
|
10
|
+
const COMMAND_NAME = process.env.XIASHE_SKILL_CLI_NAME || 'xiashe-skill';
|
|
11
|
+
const PRODUCT_NAME = process.env.XIASHE_SKILL_PRODUCT_NAME || (COMMAND_NAME === 'agentpie-skill' ? 'AgentPie' : 'XiaShe');
|
|
12
|
+
const REGISTRY_PROVIDER = process.env.XIASHE_SKILL_REGISTRY_PROVIDER || (COMMAND_NAME === 'agentpie-skill' ? 'agentpie' : 'xiashe');
|
|
13
|
+
const DEFAULT_BASE_URL = process.env.XIASHE_SKILL_DEFAULT_BASE_URL || (COMMAND_NAME === 'agentpie-skill'
|
|
14
|
+
? 'https://api.agentpie.app'
|
|
15
|
+
: 'https://api.xiashe.chat');
|
|
16
|
+
const MANIFEST_FILE = process.env.XIASHE_SKILL_MANIFEST_FILE || (COMMAND_NAME === 'agentpie-skill'
|
|
17
|
+
? 'agentpie.skill.json'
|
|
18
|
+
: 'xiashe.skill.json');
|
|
19
|
+
const WORK_DIR = process.env.XIASHE_SKILL_WORK_DIR || (COMMAND_NAME === 'agentpie-skill' ? '.agentpie' : '.xiashe');
|
|
20
|
+
const DEFAULT_REGISTRY_URL = process.env.XIASHE_SKILL_REGISTRY_URL || `${DEFAULT_BASE_URL}/registry/skill-events`;
|
|
21
|
+
const DEFAULT_CLAIM_URL = process.env.XIASHE_SKILL_CLAIM_URL || `${DEFAULT_BASE_URL}/registry/skill/claim`;
|
|
22
|
+
const DEFAULT_IGNORE = new Set([
|
|
23
|
+
'.git',
|
|
24
|
+
'.xiashe',
|
|
25
|
+
'.agentpie',
|
|
26
|
+
'node_modules',
|
|
27
|
+
'.DS_Store',
|
|
28
|
+
'.env',
|
|
29
|
+
'.env.local',
|
|
30
|
+
'.env.production',
|
|
31
|
+
'.env.development',
|
|
32
|
+
'dist',
|
|
33
|
+
'build',
|
|
34
|
+
'coverage'
|
|
35
|
+
]);
|
|
36
|
+
const MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
37
|
+
const MAX_PACKAGE_BYTES = 30 * 1024 * 1024;
|
|
38
|
+
|
|
39
|
+
function usage() {
|
|
40
|
+
return `${COMMAND_NAME} ${VERSION}
|
|
41
|
+
|
|
42
|
+
Usage:
|
|
43
|
+
${COMMAND_NAME} inspect [skill-dir] [--json]
|
|
44
|
+
${COMMAND_NAME} attach [skill-dir] --code <dynamic-code> [--name <name>]
|
|
45
|
+
${COMMAND_NAME} attach [skill-dir] --public-token <token> [--skill-id <id>] [--name <name>]
|
|
46
|
+
${COMMAND_NAME} pack [skill-dir] [--out <dir>] [--dry-run] [--json]
|
|
47
|
+
${COMMAND_NAME} prompt [skill-dir] --hub <red|generic> [--package-url <url>] [--out <file>]
|
|
48
|
+
|
|
49
|
+
Options:
|
|
50
|
+
--public-token <token> Public write token issued by ${PRODUCT_NAME} registry.
|
|
51
|
+
--code <dynamic-code> One-time ${PRODUCT_NAME} dashboard code used to claim registry identity.
|
|
52
|
+
--claim-url <url> Code claim endpoint. Defaults to XIASHE_SKILL_CLAIM_URL or ${DEFAULT_CLAIM_URL}
|
|
53
|
+
--skill-id <id> Public ${PRODUCT_NAME} Skill registry id.
|
|
54
|
+
--registry-url <url> Event endpoint written to the manifest.
|
|
55
|
+
--name <name> Skill display name override.
|
|
56
|
+
--description <text> Skill description override.
|
|
57
|
+
--hub <red|generic> Target Skill hub prompt style.
|
|
58
|
+
--package-url <url> URL an Agent can use when uploading by URL.
|
|
59
|
+
--out <path> Output package directory or prompt file.
|
|
60
|
+
--dry-run Print planned files without writing package files.
|
|
61
|
+
--json Print machine-readable JSON.
|
|
62
|
+
--help Show help.
|
|
63
|
+
--version Show version.
|
|
64
|
+
`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function fail(message, code = 1) {
|
|
68
|
+
console.error(message);
|
|
69
|
+
process.exit(code);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function parseArgs(argv) {
|
|
73
|
+
const args = { _: [], json: false, dryRun: false };
|
|
74
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
75
|
+
const value = argv[i];
|
|
76
|
+
if (value === '--help' || value === '-h') {
|
|
77
|
+
console.log(usage());
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
if (value === '--version' || value === '-v') {
|
|
81
|
+
console.log(VERSION);
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
84
|
+
if (value === '--json') {
|
|
85
|
+
args.json = true;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (value === '--dry-run') {
|
|
89
|
+
args.dryRun = true;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (value.startsWith('--')) {
|
|
93
|
+
const key = value.slice(2);
|
|
94
|
+
const next = argv[i + 1];
|
|
95
|
+
if (!next || next.startsWith('--')) {
|
|
96
|
+
fail(`Missing value for ${value}.`, 2);
|
|
97
|
+
}
|
|
98
|
+
args[key] = next;
|
|
99
|
+
i += 1;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
args._.push(value);
|
|
103
|
+
}
|
|
104
|
+
return args;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function print(value, json = false) {
|
|
108
|
+
if (json || typeof value !== 'string') {
|
|
109
|
+
console.log(JSON.stringify(value, null, 2));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
console.log(value);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function normalizeText(value, maxLength = 240) {
|
|
116
|
+
return String(value || '').trim().replace(/\s+/g, ' ').slice(0, maxLength);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function safeSkillKey(value) {
|
|
120
|
+
return normalizeText(value, 120)
|
|
121
|
+
.toLowerCase()
|
|
122
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
123
|
+
.replace(/^-+|-+$/g, '') || 'skill';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function readJsonFile(filePath) {
|
|
127
|
+
if (!existsSync(filePath)) return null;
|
|
128
|
+
try {
|
|
129
|
+
return JSON.parse(await readFile(filePath, 'utf8'));
|
|
130
|
+
} catch {
|
|
131
|
+
throw new Error(`Invalid JSON: ${filePath}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function inferSkillMetadata(root, flags = {}) {
|
|
136
|
+
const manifest = await readJsonFile(path.join(root, MANIFEST_FILE));
|
|
137
|
+
const packageJson = await readJsonFile(path.join(root, 'package.json'));
|
|
138
|
+
const skillMdPath = path.join(root, 'SKILL.md');
|
|
139
|
+
const readmePath = path.join(root, 'README.md');
|
|
140
|
+
const skillMd = existsSync(skillMdPath) ? await readFile(skillMdPath, 'utf8') : '';
|
|
141
|
+
const readme = !skillMd && existsSync(readmePath) ? await readFile(readmePath, 'utf8') : '';
|
|
142
|
+
const firstHeading = [...skillMd.matchAll(/^#\s+(.+)$/gm), ...readme.matchAll(/^#\s+(.+)$/gm)][0]?.[1];
|
|
143
|
+
const firstParagraph = (skillMd || readme)
|
|
144
|
+
.split(/\n{2,}/)
|
|
145
|
+
.map((item) => item.trim())
|
|
146
|
+
.find((item) => item && !item.startsWith('#') && !item.startsWith('---'));
|
|
147
|
+
const name = normalizeText(flags.name || manifest?.name || packageJson?.name || firstHeading || path.basename(root), 120);
|
|
148
|
+
return {
|
|
149
|
+
name,
|
|
150
|
+
skillKey: safeSkillKey(flags['skill-key'] || manifest?.skillKey || packageJson?.name || name),
|
|
151
|
+
description: normalizeText(flags.description || manifest?.description || packageJson?.description || firstParagraph || '', 500),
|
|
152
|
+
version: normalizeText(flags.version || manifest?.version || packageJson?.version || '0.1.0', 80),
|
|
153
|
+
registry: manifest?.registry || null
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function shouldIgnore(name) {
|
|
158
|
+
return DEFAULT_IGNORE.has(name) || name.endsWith('~') || name.endsWith('.tmp') || name.endsWith('.log');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function walkFiles(root, current = root, out = []) {
|
|
162
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
163
|
+
for (const entry of entries) {
|
|
164
|
+
if (shouldIgnore(entry.name)) continue;
|
|
165
|
+
const absolute = path.join(current, entry.name);
|
|
166
|
+
const relative = path.relative(root, absolute).replaceAll(path.sep, '/');
|
|
167
|
+
if (entry.isDirectory()) {
|
|
168
|
+
await walkFiles(root, absolute, out);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (!entry.isFile()) continue;
|
|
172
|
+
const info = await stat(absolute);
|
|
173
|
+
if (info.size > MAX_FILE_BYTES) continue;
|
|
174
|
+
out.push({ absolute, relative, size: info.size });
|
|
175
|
+
}
|
|
176
|
+
return out.sort((a, b) => a.relative.localeCompare(b.relative));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function hashFiles(files) {
|
|
180
|
+
const hash = createHash('sha256');
|
|
181
|
+
let totalBytes = 0;
|
|
182
|
+
for (const file of files) {
|
|
183
|
+
const bytes = await readFile(file.absolute);
|
|
184
|
+
totalBytes += bytes.length;
|
|
185
|
+
hash.update(file.relative);
|
|
186
|
+
hash.update('\0');
|
|
187
|
+
hash.update(bytes);
|
|
188
|
+
hash.update('\0');
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
sha256: hash.digest('hex'),
|
|
192
|
+
totalBytes
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function postJson(url, body, timeoutMs = 20_000) {
|
|
197
|
+
const controller = new AbortController();
|
|
198
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
199
|
+
try {
|
|
200
|
+
const response = await fetch(url, {
|
|
201
|
+
method: 'POST',
|
|
202
|
+
headers: { 'content-type': 'application/json' },
|
|
203
|
+
body: JSON.stringify(body),
|
|
204
|
+
signal: controller.signal
|
|
205
|
+
});
|
|
206
|
+
const text = await response.text();
|
|
207
|
+
let payload = {};
|
|
208
|
+
try {
|
|
209
|
+
payload = text ? JSON.parse(text) : {};
|
|
210
|
+
} catch {
|
|
211
|
+
throw new Error(`Bad JSON response from ${url}`);
|
|
212
|
+
}
|
|
213
|
+
if (!response.ok || payload.ok === false) {
|
|
214
|
+
throw new Error(payload.error || payload.message || `HTTP ${response.status}`);
|
|
215
|
+
}
|
|
216
|
+
return payload;
|
|
217
|
+
} finally {
|
|
218
|
+
clearTimeout(timer);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function inspectSkill(root, flags = {}) {
|
|
223
|
+
const absoluteRoot = path.resolve(root || '.');
|
|
224
|
+
const info = await stat(absoluteRoot).catch(() => null);
|
|
225
|
+
if (!info?.isDirectory()) {
|
|
226
|
+
throw new Error(`Skill directory not found: ${absoluteRoot}`);
|
|
227
|
+
}
|
|
228
|
+
const metadata = await inferSkillMetadata(absoluteRoot, flags);
|
|
229
|
+
const files = await walkFiles(absoluteRoot);
|
|
230
|
+
const digest = await hashFiles(files);
|
|
231
|
+
return {
|
|
232
|
+
root: absoluteRoot,
|
|
233
|
+
...metadata,
|
|
234
|
+
package: {
|
|
235
|
+
fileCount: files.length,
|
|
236
|
+
totalBytes: digest.totalBytes,
|
|
237
|
+
sha256: digest.sha256,
|
|
238
|
+
tooLarge: digest.totalBytes > MAX_PACKAGE_BYTES,
|
|
239
|
+
files: files.map((file) => ({ path: file.relative, size: file.size }))
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function writeManifest(root, flags) {
|
|
245
|
+
const inspected = await inspectSkill(root, flags);
|
|
246
|
+
const code = normalizeText(flags.code, 80);
|
|
247
|
+
const claim = code
|
|
248
|
+
? await postJson(
|
|
249
|
+
normalizeText(flags['claim-url'], 800) || DEFAULT_CLAIM_URL,
|
|
250
|
+
{
|
|
251
|
+
code,
|
|
252
|
+
skill: {
|
|
253
|
+
name: inspected.name,
|
|
254
|
+
skillKey: inspected.skillKey,
|
|
255
|
+
description: inspected.description,
|
|
256
|
+
version: inspected.version,
|
|
257
|
+
packageSha256: inspected.package.sha256,
|
|
258
|
+
packageFileCount: inspected.package.fileCount,
|
|
259
|
+
packageTotalBytes: inspected.package.totalBytes
|
|
260
|
+
},
|
|
261
|
+
device: {
|
|
262
|
+
label: normalizeText(flags['device-label'], 120) || os.hostname(),
|
|
263
|
+
platform: `${os.platform()}:${os.arch()}`
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
Number(flags['timeout-ms'] || 20_000)
|
|
267
|
+
)
|
|
268
|
+
: null;
|
|
269
|
+
const publicToken = normalizeText(flags['public-token'] || claim?.publicToken, 512);
|
|
270
|
+
if (!publicToken) {
|
|
271
|
+
throw new Error('Missing --code or --public-token.');
|
|
272
|
+
}
|
|
273
|
+
const now = new Date().toISOString();
|
|
274
|
+
const manifest = {
|
|
275
|
+
schemaVersion: `${REGISTRY_PROVIDER}.skill.registry.v1`,
|
|
276
|
+
name: normalizeText(claim?.name, 120) || inspected.name,
|
|
277
|
+
skillKey: safeSkillKey(claim?.skillKey || inspected.skillKey),
|
|
278
|
+
description: normalizeText(claim?.description, 500) || inspected.description,
|
|
279
|
+
version: normalizeText(claim?.version, 80) || inspected.version,
|
|
280
|
+
registry: {
|
|
281
|
+
provider: REGISTRY_PROVIDER,
|
|
282
|
+
skillId: normalizeText(flags['skill-id'] || claim?.skillId, 160) || null,
|
|
283
|
+
publicToken,
|
|
284
|
+
registryUrl: normalizeText(flags['registry-url'] || claim?.registryUrl, 800) || DEFAULT_REGISTRY_URL,
|
|
285
|
+
analytics: {
|
|
286
|
+
enabled: true,
|
|
287
|
+
allowedEvents: [
|
|
288
|
+
'package_created',
|
|
289
|
+
'upload_prompt_generated',
|
|
290
|
+
'hub_upload_attempted',
|
|
291
|
+
'hub_upload_succeeded',
|
|
292
|
+
'hub_download_reported',
|
|
293
|
+
'hub_install_reported',
|
|
294
|
+
'runtime_install_seen',
|
|
295
|
+
'skill_invoked',
|
|
296
|
+
'skill_completed',
|
|
297
|
+
'skill_failed'
|
|
298
|
+
]
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
package: {
|
|
302
|
+
sha256: inspected.package.sha256,
|
|
303
|
+
fileCount: inspected.package.fileCount,
|
|
304
|
+
totalBytes: inspected.package.totalBytes,
|
|
305
|
+
generatedAt: now
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
const manifestPath = path.join(inspected.root, MANIFEST_FILE);
|
|
309
|
+
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
|
|
310
|
+
return { ok: true, manifestPath, manifest };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function packSkill(root, flags) {
|
|
314
|
+
const inspected = await inspectSkill(root, flags);
|
|
315
|
+
if (inspected.package.tooLarge) {
|
|
316
|
+
throw new Error(`Package is too large: ${inspected.package.totalBytes} bytes. Max ${MAX_PACKAGE_BYTES}.`);
|
|
317
|
+
}
|
|
318
|
+
const outDir = path.resolve(flags.out || path.join(inspected.root, WORK_DIR, 'skill-package'));
|
|
319
|
+
if (flags.dryRun) {
|
|
320
|
+
return { ok: true, dryRun: true, outDir, ...inspected };
|
|
321
|
+
}
|
|
322
|
+
await mkdir(outDir, { recursive: true });
|
|
323
|
+
for (const file of inspected.package.files) {
|
|
324
|
+
const source = path.join(inspected.root, file.path);
|
|
325
|
+
const target = path.join(outDir, file.path);
|
|
326
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
327
|
+
await copyFile(source, target);
|
|
328
|
+
}
|
|
329
|
+
const packageManifest = {
|
|
330
|
+
schemaVersion: 'xiashe.skill.package.v1',
|
|
331
|
+
createdAt: new Date().toISOString(),
|
|
332
|
+
createdBy: `${os.platform()}:${os.hostname()}`,
|
|
333
|
+
skill: {
|
|
334
|
+
name: inspected.name,
|
|
335
|
+
skillKey: inspected.skillKey,
|
|
336
|
+
description: inspected.description,
|
|
337
|
+
version: inspected.version
|
|
338
|
+
},
|
|
339
|
+
registry: inspected.registry,
|
|
340
|
+
package: inspected.package
|
|
341
|
+
};
|
|
342
|
+
await writeFile(path.join(outDir, 'xiashe.package.json'), `${JSON.stringify(packageManifest, null, 2)}\n`, { mode: 0o600 });
|
|
343
|
+
return { ok: true, outDir, manifestPath: path.join(outDir, 'xiashe.package.json'), ...inspected };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function uploadPrompt(inspected, flags) {
|
|
347
|
+
const hub = normalizeText(flags.hub || 'generic', 40).toLowerCase();
|
|
348
|
+
const packageUrl = normalizeText(flags['package-url'], 1000);
|
|
349
|
+
const registry = inspected.registry || {};
|
|
350
|
+
const registryUrl = registry.registryUrl || DEFAULT_REGISTRY_URL;
|
|
351
|
+
const eventPayload = {
|
|
352
|
+
publicToken: registry.publicToken || '<xiashe public token from xiashe.skill.json>',
|
|
353
|
+
eventType: 'hub_upload_succeeded',
|
|
354
|
+
hub,
|
|
355
|
+
skillKey: inspected.skillKey,
|
|
356
|
+
packageSha256: inspected.package.sha256,
|
|
357
|
+
platformSkillUrl: '<published skill url>',
|
|
358
|
+
scenario: '<short usage scenario if the hub asks for one>'
|
|
359
|
+
};
|
|
360
|
+
const runtimePayload = {
|
|
361
|
+
publicToken: registry.publicToken || '<public token from registry manifest>',
|
|
362
|
+
eventType: 'skill_invoked',
|
|
363
|
+
eventSource: 'runtime',
|
|
364
|
+
runtimeKind: '<mcp|api|agent|webhook|platform>',
|
|
365
|
+
hub,
|
|
366
|
+
skillKey: inspected.skillKey,
|
|
367
|
+
installationId: '<stable anonymous install id>',
|
|
368
|
+
invocationId: '<unique invocation id>',
|
|
369
|
+
scenario: '<short usage scenario label only>'
|
|
370
|
+
};
|
|
371
|
+
const hubLine = hub === 'red'
|
|
372
|
+
? '请按照小红书创作服务平台 Red Skill 的上传流程发布。'
|
|
373
|
+
: '请按照目标 Skill Hub 的上传流程发布。';
|
|
374
|
+
const sourceLine = packageUrl
|
|
375
|
+
? `优先使用这个包或源码 URL 上传:${packageUrl}`
|
|
376
|
+
: `请使用当前目录或我提供的 Skill 包目录上传,Skill 根目录必须包含 SKILL.md 或 README.md。`;
|
|
377
|
+
return [
|
|
378
|
+
`你现在要帮我发布一个 Skill:${inspected.name}`,
|
|
379
|
+
'',
|
|
380
|
+
hubLine,
|
|
381
|
+
sourceLine,
|
|
382
|
+
'',
|
|
383
|
+
'发布要求:',
|
|
384
|
+
`- Skill 名称:${inspected.name}`,
|
|
385
|
+
`- Skill Key:${inspected.skillKey}`,
|
|
386
|
+
`- 版本:${inspected.version}`,
|
|
387
|
+
`- 描述:${inspected.description || '<从 SKILL.md/README.md 中提炼>'}`,
|
|
388
|
+
`- 包 SHA256:${inspected.package.sha256}`,
|
|
389
|
+
'- 不要读取或上传 .env、密钥、浏览器数据、SSH key、系统目录或 node_modules。',
|
|
390
|
+
'- 不要新增 postinstall hook、后台常驻进程、隐藏脚本或混淆逻辑。',
|
|
391
|
+
`- 如果平台要求安全说明,请明确说明这是用户显式发布的 Skill 包,${MANIFEST_FILE} 只包含公开 registry 归因和可关闭的 analytics 声明。`,
|
|
392
|
+
`- 如果目标平台支持工具调用、MCP server、webhook 或 Skill runtime HTTP 请求,请在 Skill 被调用时向 ${PRODUCT_NAME} registry 发送聚合事件:skill_invoked / skill_completed / skill_failed。`,
|
|
393
|
+
'- 事件里只放公开元数据、聚合状态和简短场景标签,不要发送用户输入原文、私密业务内容、账号凭据或文件内容。',
|
|
394
|
+
'- installationId 必须是稳定的匿名实例 ID,用于统计唯一安装实例;不要使用邮箱、手机号、真实用户名或设备序列号。',
|
|
395
|
+
'',
|
|
396
|
+
`发布成功后,请把公开链接和平台返回的信息回传给${PRODUCT_NAME} registry。只发送公开元数据和聚合事件,不发送用户内容、私密输入或账号凭据。`,
|
|
397
|
+
'',
|
|
398
|
+
`POST ${registryUrl}`,
|
|
399
|
+
'Content-Type: application/json',
|
|
400
|
+
'',
|
|
401
|
+
JSON.stringify(eventPayload, null, 2),
|
|
402
|
+
'',
|
|
403
|
+
'如果你能修改 Skill runtime,请在调用开始时发送:',
|
|
404
|
+
'',
|
|
405
|
+
`POST ${registryUrl}`,
|
|
406
|
+
'Content-Type: application/json',
|
|
407
|
+
'',
|
|
408
|
+
JSON.stringify(runtimePayload, null, 2),
|
|
409
|
+
'',
|
|
410
|
+
'调用完成或失败时,用同一个 invocationId 发送 skill_completed 或 skill_failed。'
|
|
411
|
+
].join('\n');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function writePrompt(root, flags) {
|
|
415
|
+
if (!flags.hub) {
|
|
416
|
+
throw new Error('Missing --hub <red|generic>.');
|
|
417
|
+
}
|
|
418
|
+
const inspected = await inspectSkill(root, flags);
|
|
419
|
+
const prompt = uploadPrompt(inspected, flags);
|
|
420
|
+
if (flags.out) {
|
|
421
|
+
const outPath = path.resolve(flags.out);
|
|
422
|
+
await mkdir(path.dirname(outPath), { recursive: true });
|
|
423
|
+
await writeFile(outPath, `${prompt}\n`, { mode: 0o600 });
|
|
424
|
+
return { ok: true, outPath, prompt };
|
|
425
|
+
}
|
|
426
|
+
return prompt;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async function main() {
|
|
430
|
+
const [command = '', ...commandArgs] = process.argv.slice(2);
|
|
431
|
+
if (!command || command === '--help' || command === '-h') {
|
|
432
|
+
console.log(usage());
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (command === '--version' || command === '-v') {
|
|
436
|
+
console.log(VERSION);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
let root = '.';
|
|
440
|
+
if (commandArgs[0] && !commandArgs[0].startsWith('--')) {
|
|
441
|
+
root = commandArgs.shift();
|
|
442
|
+
}
|
|
443
|
+
const flags = parseArgs(commandArgs);
|
|
444
|
+
try {
|
|
445
|
+
if (command === 'inspect') {
|
|
446
|
+
const result = await inspectSkill(root, flags);
|
|
447
|
+
print(result, flags.json);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (command === 'attach' || command === 'link') {
|
|
451
|
+
const result = await writeManifest(root, flags);
|
|
452
|
+
print(flags.json ? result : `Wrote ${result.manifestPath}`, flags.json);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (command === 'pack') {
|
|
456
|
+
const result = await packSkill(root, flags);
|
|
457
|
+
print(flags.json ? result : `Prepared package directory: ${result.outDir}`, flags.json);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (command === 'prompt') {
|
|
461
|
+
const result = await writePrompt(root, flags);
|
|
462
|
+
if (typeof result === 'string' || flags.json) {
|
|
463
|
+
print(result, flags.json);
|
|
464
|
+
} else {
|
|
465
|
+
print(`Wrote ${result.outPath}`, false);
|
|
466
|
+
}
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
fail(`Unknown command: ${command}\n\n${usage()}`, 2);
|
|
470
|
+
} catch (error) {
|
|
471
|
+
fail(error instanceof Error ? error.message : String(error || 'xiashe-skill failed'), 1);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
void main();
|