@xiashe/skill 0.1.0 → 0.1.1
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 +8 -5
- package/bin/xiashe-skill.mjs +348 -59
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,19 +9,22 @@ This package is intentionally separate from the full `@xiashe/cli` product CLI a
|
|
|
9
9
|
`xiashe-skill` helps creators prepare a Skill folder for third-party Skill hubs:
|
|
10
10
|
|
|
11
11
|
- inspect a local Skill project
|
|
12
|
+
- run one Agent-friendly registry setup command
|
|
12
13
|
- write an explicit `xiashe.skill.json` registry manifest
|
|
13
|
-
-
|
|
14
|
-
- generate
|
|
14
|
+
- generate an upload handoff prompt that the creator can hand to an Agent
|
|
15
|
+
- generate optional runtime analytics snippets
|
|
15
16
|
|
|
16
|
-
It does not install background services, run postinstall hooks, read secrets, or upload to third-party hubs.
|
|
17
|
+
It does not create upload packages, copy source directories, install background services, run postinstall hooks, read secrets, or upload to third-party hubs. The creator's Agent is responsible for packaging and uploading according to the target Skill hub's official rules.
|
|
17
18
|
|
|
18
19
|
## Local development
|
|
19
20
|
|
|
20
21
|
```bash
|
|
21
22
|
node packages/xiashe-skill-cli/bin/xiashe-skill.mjs --help
|
|
22
23
|
node packages/xiashe-skill-cli/bin/xiashe-skill.mjs inspect .
|
|
24
|
+
node packages/xiashe-skill-cli/bin/xiashe-skill.mjs setup . --code XS-XXXX-XXXX --hub red
|
|
23
25
|
node packages/xiashe-skill-cli/bin/xiashe-skill.mjs attach . --code XS-XXXX-XXXX
|
|
24
26
|
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
|
|
26
|
-
node packages/xiashe-skill-cli/bin/xiashe-skill.mjs
|
|
27
|
+
node packages/xiashe-skill-cli/bin/xiashe-skill.mjs prompt . --hub red --source-url https://example.com/my-skill-repo
|
|
28
|
+
node packages/xiashe-skill-cli/bin/xiashe-skill.mjs snippet . --target js
|
|
29
|
+
node packages/xiashe-skill-cli/bin/xiashe-skill.mjs track . --event skill_invoked --dry-run --json
|
|
27
30
|
```
|
package/bin/xiashe-skill.mjs
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import { existsSync } from 'node:fs';
|
|
5
|
-
import {
|
|
5
|
+
import { mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
6
6
|
import os from 'node:os';
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
|
|
9
|
-
const VERSION = '0.1.
|
|
9
|
+
const VERSION = '0.1.1';
|
|
10
10
|
const COMMAND_NAME = process.env.XIASHE_SKILL_CLI_NAME || 'xiashe-skill';
|
|
11
11
|
const PRODUCT_NAME = process.env.XIASHE_SKILL_PRODUCT_NAME || (COMMAND_NAME === 'agentpie-skill' ? 'AgentPie' : 'XiaShe');
|
|
12
12
|
const REGISTRY_PROVIDER = process.env.XIASHE_SKILL_REGISTRY_PROVIDER || (COMMAND_NAME === 'agentpie-skill' ? 'agentpie' : 'xiashe');
|
|
@@ -34,17 +34,20 @@ const DEFAULT_IGNORE = new Set([
|
|
|
34
34
|
'coverage'
|
|
35
35
|
]);
|
|
36
36
|
const MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
37
|
-
const
|
|
37
|
+
const MAX_SOURCE_BYTES = 30 * 1024 * 1024;
|
|
38
38
|
|
|
39
39
|
function usage() {
|
|
40
40
|
return `${COMMAND_NAME} ${VERSION}
|
|
41
41
|
|
|
42
42
|
Usage:
|
|
43
43
|
${COMMAND_NAME} inspect [skill-dir] [--json]
|
|
44
|
+
${COMMAND_NAME} setup [skill-dir] --code <dynamic-code> --hub <red|clawhub|skillhub|generic>
|
|
45
|
+
${COMMAND_NAME} setup [skill-dir] --public-token <token> --skill-id <id> --hub <red|clawhub|skillhub|generic>
|
|
44
46
|
${COMMAND_NAME} attach [skill-dir] --code <dynamic-code> [--name <name>]
|
|
45
47
|
${COMMAND_NAME} attach [skill-dir] --public-token <token> [--skill-id <id>] [--name <name>]
|
|
46
|
-
${COMMAND_NAME}
|
|
47
|
-
${COMMAND_NAME}
|
|
48
|
+
${COMMAND_NAME} prompt [skill-dir] --hub <red|clawhub|skillhub|generic> [--source-url <url>] [--out <file>]
|
|
49
|
+
${COMMAND_NAME} snippet [skill-dir] [--target js|curl] [--out <file>]
|
|
50
|
+
${COMMAND_NAME} track [skill-dir] --event <event> [--hub <hub>] [--installation-id <id>] [--invocation-id <id>]
|
|
48
51
|
|
|
49
52
|
Options:
|
|
50
53
|
--public-token <token> Public write token issued by ${PRODUCT_NAME} registry.
|
|
@@ -54,10 +57,23 @@ Options:
|
|
|
54
57
|
--registry-url <url> Event endpoint written to the manifest.
|
|
55
58
|
--name <name> Skill display name override.
|
|
56
59
|
--description <text> Skill description override.
|
|
57
|
-
--hub <
|
|
58
|
-
--
|
|
59
|
-
--
|
|
60
|
-
--
|
|
60
|
+
--hub <hub> Target Skill hub prompt style: red, clawhub, skillhub, or generic.
|
|
61
|
+
--event <event> Runtime event to submit for testing.
|
|
62
|
+
--installation-id <id> Stable anonymous install id for runtime analytics.
|
|
63
|
+
--invocation-id <id> Unique invocation id for one skill call.
|
|
64
|
+
--scenario <label> Short scenario label.
|
|
65
|
+
--campaign <label> Campaign or promotion label.
|
|
66
|
+
--platform-skill-url <url> Published Skill URL for hub_upload_succeeded events.
|
|
67
|
+
--platform-prompt-file <p> Official third-party hub prompt file to embed in the handoff.
|
|
68
|
+
--platform-command <cmd> Official third-party hub CLI/upload command to include.
|
|
69
|
+
--manifest-path <path> Registry manifest output path. Defaults to ${MANIFEST_FILE} for attach, ${WORK_DIR}/${MANIFEST_FILE} for setup.
|
|
70
|
+
--source-url <url> User-provided source URL an Agent can use during third-party upload.
|
|
71
|
+
--package-url <url> Deprecated alias for --source-url.
|
|
72
|
+
--out <path> Output prompt or snippet file.
|
|
73
|
+
--out-dir <path> Output directory for setup artifacts. Defaults to ${WORK_DIR}/.
|
|
74
|
+
--no-snippet Setup should skip writing the runtime analytics snippet.
|
|
75
|
+
--no-track Setup should skip sending upload_prompt_generated.
|
|
76
|
+
--dry-run Print planned event payloads without network writes.
|
|
61
77
|
--json Print machine-readable JSON.
|
|
62
78
|
--help Show help.
|
|
63
79
|
--version Show version.
|
|
@@ -71,6 +87,7 @@ function fail(message, code = 1) {
|
|
|
71
87
|
|
|
72
88
|
function parseArgs(argv) {
|
|
73
89
|
const args = { _: [], json: false, dryRun: false };
|
|
90
|
+
const booleanFlags = new Set(['no-snippet', 'no-track']);
|
|
74
91
|
for (let i = 0; i < argv.length; i += 1) {
|
|
75
92
|
const value = argv[i];
|
|
76
93
|
if (value === '--help' || value === '-h') {
|
|
@@ -91,6 +108,10 @@ function parseArgs(argv) {
|
|
|
91
108
|
}
|
|
92
109
|
if (value.startsWith('--')) {
|
|
93
110
|
const key = value.slice(2);
|
|
111
|
+
if (booleanFlags.has(key)) {
|
|
112
|
+
args[key] = true;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
94
115
|
const next = argv[i + 1];
|
|
95
116
|
if (!next || next.startsWith('--')) {
|
|
96
117
|
fail(`Missing value for ${value}.`, 2);
|
|
@@ -133,7 +154,11 @@ async function readJsonFile(filePath) {
|
|
|
133
154
|
}
|
|
134
155
|
|
|
135
156
|
async function inferSkillMetadata(root, flags = {}) {
|
|
136
|
-
const
|
|
157
|
+
const manifestPath = flags['manifest-path']
|
|
158
|
+
? path.resolve(flags['manifest-path'])
|
|
159
|
+
: path.join(root, MANIFEST_FILE);
|
|
160
|
+
const localManifestPath = path.join(root, WORK_DIR, MANIFEST_FILE);
|
|
161
|
+
const manifest = await readJsonFile(manifestPath) || await readJsonFile(localManifestPath);
|
|
137
162
|
const packageJson = await readJsonFile(path.join(root, 'package.json'));
|
|
138
163
|
const skillMdPath = path.join(root, 'SKILL.md');
|
|
139
164
|
const readmePath = path.join(root, 'README.md');
|
|
@@ -235,7 +260,7 @@ async function inspectSkill(root, flags = {}) {
|
|
|
235
260
|
fileCount: files.length,
|
|
236
261
|
totalBytes: digest.totalBytes,
|
|
237
262
|
sha256: digest.sha256,
|
|
238
|
-
tooLarge: digest.totalBytes >
|
|
263
|
+
tooLarge: digest.totalBytes > MAX_SOURCE_BYTES,
|
|
239
264
|
files: files.map((file) => ({ path: file.relative, size: file.size }))
|
|
240
265
|
}
|
|
241
266
|
};
|
|
@@ -285,7 +310,6 @@ async function writeManifest(root, flags) {
|
|
|
285
310
|
analytics: {
|
|
286
311
|
enabled: true,
|
|
287
312
|
allowedEvents: [
|
|
288
|
-
'package_created',
|
|
289
313
|
'upload_prompt_generated',
|
|
290
314
|
'hub_upload_attempted',
|
|
291
315
|
'hub_upload_succeeded',
|
|
@@ -298,54 +322,104 @@ async function writeManifest(root, flags) {
|
|
|
298
322
|
]
|
|
299
323
|
}
|
|
300
324
|
},
|
|
301
|
-
|
|
325
|
+
sourceFingerprint: {
|
|
302
326
|
sha256: inspected.package.sha256,
|
|
303
327
|
fileCount: inspected.package.fileCount,
|
|
304
328
|
totalBytes: inspected.package.totalBytes,
|
|
305
329
|
generatedAt: now
|
|
306
330
|
}
|
|
307
331
|
};
|
|
308
|
-
const manifestPath = path
|
|
332
|
+
const manifestPath = flags['manifest-path']
|
|
333
|
+
? path.resolve(flags['manifest-path'])
|
|
334
|
+
: flags['local-manifest']
|
|
335
|
+
? path.join(inspected.root, WORK_DIR, MANIFEST_FILE)
|
|
336
|
+
: path.join(inspected.root, MANIFEST_FILE);
|
|
337
|
+
await mkdir(path.dirname(manifestPath), { recursive: true });
|
|
309
338
|
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
|
|
310
339
|
return { ok: true, manifestPath, manifest };
|
|
311
340
|
}
|
|
312
341
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
throw new Error(`Package is too large: ${inspected.package.totalBytes} bytes. Max ${MAX_PACKAGE_BYTES}.`);
|
|
342
|
+
function hubInstruction(hub) {
|
|
343
|
+
if (hub === 'red') {
|
|
344
|
+
return 'Red Skill 的官方上传 prompt / 页面流程是主流程。请先从小红书创作服务平台获取或使用用户提供的 Red Skill 上传 prompt,再把下面的虾舍 registry 要求作为附加说明合并进去。';
|
|
317
345
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
return { ok: true, dryRun: true, outDir, ...inspected };
|
|
346
|
+
if (hub === 'clawhub') {
|
|
347
|
+
return 'ClawHub 的官方 CLI、prompt 或页面流程是主流程。请先确认 ClawHub 当前要求,再把下面的虾舍 registry 要求作为附加说明合并进去。';
|
|
321
348
|
}
|
|
322
|
-
|
|
323
|
-
|
|
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);
|
|
349
|
+
if (hub === 'skillhub') {
|
|
350
|
+
return 'SkillHub 的官方 CLI、prompt 或页面流程是主流程。请先确认 SkillHub 当前要求,再把下面的虾舍 registry 要求作为附加说明合并进去。';
|
|
328
351
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
|
|
352
|
+
return '目标 Skill Hub 的官方 CLI、prompt 或页面流程是主流程。请先确认平台当前要求,再把下面的虾舍 registry 要求作为附加说明合并进去。';
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function platformPromptPlaceholder(hub) {
|
|
356
|
+
if (hub === 'red') {
|
|
357
|
+
return '如果用户还没有提供 Red Skill 官方上传 prompt,请先请用户从小红书创作服务平台复制该 prompt;不要用虾舍 prompt 替代 Red Skill 官方 prompt。';
|
|
358
|
+
}
|
|
359
|
+
if (hub === 'clawhub') {
|
|
360
|
+
return '如果 ClawHub 提供官方 CLI 命令或上传 prompt,请优先使用官方命令/prompt;不要用虾舍 prompt 替代 ClawHub 官方流程。';
|
|
361
|
+
}
|
|
362
|
+
if (hub === 'skillhub') {
|
|
363
|
+
return '如果 SkillHub 提供官方 CLI 命令或上传 prompt,请优先使用官方命令/prompt;不要用虾舍 prompt 替代 SkillHub 官方流程。';
|
|
364
|
+
}
|
|
365
|
+
return '如果目标平台提供官方 CLI 命令或上传 prompt,请优先使用官方命令/prompt;不要用虾舍 prompt 替代平台官方流程。';
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function uploadCompatibilityLines(hub, manifestFile) {
|
|
369
|
+
const localManifest = `${WORK_DIR}/${manifestFile}`;
|
|
370
|
+
if (hub === 'red') {
|
|
371
|
+
return [
|
|
372
|
+
'Red Skill 兼容性要求:',
|
|
373
|
+
'- Red Skill 如果只接受 Markdown/TXT,请不要把 JSON manifest 当作源码文件上传。',
|
|
374
|
+
`- ${localManifest} 保留在用户本地用于虾舍 registry claim 和 analytics;只有在 Red Skill 官方流程明确允许附加 JSON 文件时才上传。`,
|
|
375
|
+
'- 如需在 Red Skill 页面披露 registry,请把下面的“公开披露文本”写入 Skill 介绍、README 或平台允许的 Markdown/TXT 字段。'
|
|
376
|
+
];
|
|
377
|
+
}
|
|
378
|
+
return [
|
|
379
|
+
'第三方平台兼容性要求:',
|
|
380
|
+
`- 如果目标平台不接受 ${localManifest} 这类 JSON manifest,请不要强行上传该文件。`,
|
|
381
|
+
'- 保留本地 registry manifest,并把“公开披露文本”写入平台允许的说明字段、README、SKILL.md 或 Markdown/TXT 附件。',
|
|
382
|
+
'- 如果平台支持额外 manifest / metadata 文件,可以在提交前向用户确认是否一并上传。'
|
|
383
|
+
];
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function publicDisclosure(inspected, registryUrl) {
|
|
387
|
+
return [
|
|
388
|
+
'公开披露文本:',
|
|
389
|
+
'```markdown',
|
|
390
|
+
`### ${PRODUCT_NAME} Registry / Analytics`,
|
|
391
|
+
'',
|
|
392
|
+
`This Skill is registered by its creator with ${PRODUCT_NAME} for public attribution and aggregated analytics.`,
|
|
393
|
+
'',
|
|
394
|
+
`- Registry provider: ${REGISTRY_PROVIDER}`,
|
|
395
|
+
`- Skill key: ${inspected.skillKey}`,
|
|
396
|
+
`- Registry event endpoint: ${registryUrl}`,
|
|
397
|
+
'- Events, if enabled by the runtime/platform, should contain only public metadata and aggregate status.',
|
|
398
|
+
'- Do not send user prompts, private files, credentials, environment variables, account tokens, or raw business content.',
|
|
399
|
+
'- Analytics can be removed by deleting the registry manifest/snippet or disabling it in the creator dashboard.',
|
|
400
|
+
'```'
|
|
401
|
+
];
|
|
344
402
|
}
|
|
345
403
|
|
|
346
|
-
function
|
|
404
|
+
async function readPlatformPrompt(flags) {
|
|
405
|
+
const promptPath = normalizeText(flags['platform-prompt-file'], 1000);
|
|
406
|
+
if (!promptPath) return '';
|
|
407
|
+
const absolute = path.resolve(promptPath);
|
|
408
|
+
const info = await stat(absolute).catch(() => null);
|
|
409
|
+
if (!info?.isFile()) {
|
|
410
|
+
throw new Error(`Platform prompt file not found: ${absolute}`);
|
|
411
|
+
}
|
|
412
|
+
if (info.size > MAX_FILE_BYTES) {
|
|
413
|
+
throw new Error(`Platform prompt file is too large: ${absolute}`);
|
|
414
|
+
}
|
|
415
|
+
return readFile(absolute, 'utf8');
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async function uploadPrompt(inspected, flags) {
|
|
347
419
|
const hub = normalizeText(flags.hub || 'generic', 40).toLowerCase();
|
|
348
|
-
const
|
|
420
|
+
const sourceUrl = normalizeText(flags['source-url'] || flags['package-url'], 1000);
|
|
421
|
+
const platformCommand = normalizeText(flags['platform-command'], 1000);
|
|
422
|
+
const platformPrompt = await readPlatformPrompt(flags);
|
|
349
423
|
const registry = inspected.registry || {};
|
|
350
424
|
const registryUrl = registry.registryUrl || DEFAULT_REGISTRY_URL;
|
|
351
425
|
const eventPayload = {
|
|
@@ -366,29 +440,51 @@ function uploadPrompt(inspected, flags) {
|
|
|
366
440
|
skillKey: inspected.skillKey,
|
|
367
441
|
installationId: '<stable anonymous install id>',
|
|
368
442
|
invocationId: '<unique invocation id>',
|
|
369
|
-
scenario: '<short usage scenario label only>'
|
|
443
|
+
scenario: '<short usage scenario label only>',
|
|
444
|
+
sourceSurface: '<profile|hub|agent|direct>',
|
|
445
|
+
campaign: '<optional promotion label>'
|
|
370
446
|
};
|
|
371
|
-
const hubLine = hub
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
447
|
+
const hubLine = hubInstruction(hub);
|
|
448
|
+
const sourceLine = sourceUrl
|
|
449
|
+
? `你可以使用用户明确提供的源码或仓库 URL:${sourceUrl}`
|
|
450
|
+
: `请在用户当前 Skill 项目根目录内操作;最终上传形态必须由你按目标 Skill 平台规则生成。`;
|
|
451
|
+
const officialFlowLines = [
|
|
452
|
+
'第三方平台官方流程:',
|
|
453
|
+
`- ${hubLine}`,
|
|
454
|
+
'- 平台官方 prompt / CLI / 页面表单是上传依据;虾舍只提供 registry 和 analytics 附加要求。',
|
|
455
|
+
'- 如果平台官方要求与虾舍附加说明冲突,以平台官方上传要求为准;不要为了 analytics 绕过平台规则。',
|
|
456
|
+
platformCommand ? `- 用户提供的官方命令:${platformCommand}` : `- ${platformPromptPlaceholder(hub)}`
|
|
457
|
+
];
|
|
458
|
+
if (platformPrompt) {
|
|
459
|
+
officialFlowLines.push('', '用户提供的第三方平台官方 prompt:', '```text', platformPrompt.trim(), '```');
|
|
460
|
+
}
|
|
461
|
+
const compatibilityLines = uploadCompatibilityLines(hub, MANIFEST_FILE);
|
|
462
|
+
const disclosureLines = publicDisclosure(inspected, registryUrl);
|
|
377
463
|
return [
|
|
378
|
-
|
|
464
|
+
`你现在要帮用户把 Skill 接入 ${PRODUCT_NAME} registry,并按第三方平台官方流程发布:${inspected.name}`,
|
|
465
|
+
'',
|
|
466
|
+
...officialFlowLines,
|
|
379
467
|
'',
|
|
380
|
-
hubLine,
|
|
381
468
|
sourceLine,
|
|
382
469
|
'',
|
|
383
|
-
|
|
470
|
+
`${PRODUCT_NAME} 职责边界:`,
|
|
471
|
+
`- ${PRODUCT_NAME} 只负责 registry manifest、公开归因和 analytics 回传说明,不负责构建、打包、上传或审核这个 Skill。`,
|
|
472
|
+
'- 最终上传文件、目录结构、压缩包、表单字段和上线版本,必须由你按照目标 Skill 平台的官方要求处理。',
|
|
473
|
+
'- 如果目标平台要求重新整理源码、生成 markdown、压缩包或填写表单,请你在用户授权范围内完成,并在提交前向用户展示将上传的文件和字段。',
|
|
474
|
+
'',
|
|
475
|
+
...compatibilityLines,
|
|
476
|
+
'',
|
|
477
|
+
...disclosureLines,
|
|
478
|
+
'',
|
|
479
|
+
`${PRODUCT_NAME} registry 附加要求:`,
|
|
384
480
|
`- Skill 名称:${inspected.name}`,
|
|
385
481
|
`- Skill Key:${inspected.skillKey}`,
|
|
386
482
|
`- 版本:${inspected.version}`,
|
|
387
483
|
`- 描述:${inspected.description || '<从 SKILL.md/README.md 中提炼>'}`,
|
|
388
|
-
`-
|
|
484
|
+
`- 源码指纹 SHA256:${inspected.package.sha256}`,
|
|
389
485
|
'- 不要读取或上传 .env、密钥、浏览器数据、SSH key、系统目录或 node_modules。',
|
|
390
486
|
'- 不要新增 postinstall hook、后台常驻进程、隐藏脚本或混淆逻辑。',
|
|
391
|
-
`- 如果平台要求安全说明,请明确说明这是用户显式发布的 Skill
|
|
487
|
+
`- 如果平台要求安全说明,请明确说明这是用户显式发布的 Skill 项目,${MANIFEST_FILE} 只包含公开 registry 归因和可关闭的 analytics 声明。`,
|
|
392
488
|
`- 如果目标平台支持工具调用、MCP server、webhook 或 Skill runtime HTTP 请求,请在 Skill 被调用时向 ${PRODUCT_NAME} registry 发送聚合事件:skill_invoked / skill_completed / skill_failed。`,
|
|
393
489
|
'- 事件里只放公开元数据、聚合状态和简短场景标签,不要发送用户输入原文、私密业务内容、账号凭据或文件内容。',
|
|
394
490
|
'- installationId 必须是稳定的匿名实例 ID,用于统计唯一安装实例;不要使用邮箱、手机号、真实用户名或设备序列号。',
|
|
@@ -416,7 +512,7 @@ async function writePrompt(root, flags) {
|
|
|
416
512
|
throw new Error('Missing --hub <red|generic>.');
|
|
417
513
|
}
|
|
418
514
|
const inspected = await inspectSkill(root, flags);
|
|
419
|
-
const prompt = uploadPrompt(inspected, flags);
|
|
515
|
+
const prompt = await uploadPrompt(inspected, flags);
|
|
420
516
|
if (flags.out) {
|
|
421
517
|
const outPath = path.resolve(flags.out);
|
|
422
518
|
await mkdir(path.dirname(outPath), { recursive: true });
|
|
@@ -426,6 +522,162 @@ async function writePrompt(root, flags) {
|
|
|
426
522
|
return prompt;
|
|
427
523
|
}
|
|
428
524
|
|
|
525
|
+
async function writeRuntimeSnippet(root, flags) {
|
|
526
|
+
const inspected = await inspectSkill(root, flags);
|
|
527
|
+
const registry = inspected.registry || {};
|
|
528
|
+
const registryUrl = registry.registryUrl || DEFAULT_REGISTRY_URL;
|
|
529
|
+
const publicToken = registry.publicToken || '<public token from registry manifest>';
|
|
530
|
+
const target = normalizeText(flags.target || 'js', 20).toLowerCase();
|
|
531
|
+
const snippet = target === 'curl'
|
|
532
|
+
? [
|
|
533
|
+
`curl -X POST '${registryUrl}' \\`,
|
|
534
|
+
" -H 'content-type: application/json' \\",
|
|
535
|
+
' -d ' + JSON.stringify(JSON.stringify({
|
|
536
|
+
publicToken,
|
|
537
|
+
eventType: 'skill_invoked',
|
|
538
|
+
eventSource: 'runtime',
|
|
539
|
+
runtimeKind: 'mcp',
|
|
540
|
+
hub: '<hub>',
|
|
541
|
+
skillKey: inspected.skillKey,
|
|
542
|
+
installationId: '<anonymous-stable-install-id>',
|
|
543
|
+
invocationId: '<unique-call-id>',
|
|
544
|
+
scenario: '<short-scenario-label>'
|
|
545
|
+
}))
|
|
546
|
+
].join('\n')
|
|
547
|
+
: [
|
|
548
|
+
`const XIASHE_SKILL_REGISTRY_URL = ${JSON.stringify(registryUrl)};`,
|
|
549
|
+
`const XIASHE_SKILL_PUBLIC_TOKEN = ${JSON.stringify(publicToken)};`,
|
|
550
|
+
'',
|
|
551
|
+
'export async function trackSkillEvent(eventType, options = {}) {',
|
|
552
|
+
' await fetch(XIASHE_SKILL_REGISTRY_URL, {',
|
|
553
|
+
" method: 'POST',",
|
|
554
|
+
" headers: { 'content-type': 'application/json' },",
|
|
555
|
+
' body: JSON.stringify({',
|
|
556
|
+
' publicToken: XIASHE_SKILL_PUBLIC_TOKEN,',
|
|
557
|
+
' eventType,',
|
|
558
|
+
" eventSource: options.eventSource || 'runtime',",
|
|
559
|
+
" runtimeKind: options.runtimeKind || 'mcp',",
|
|
560
|
+
` skillKey: ${JSON.stringify(inspected.skillKey)},`,
|
|
561
|
+
' hub: options.hub,',
|
|
562
|
+
' installationId: options.installationId,',
|
|
563
|
+
' invocationId: options.invocationId,',
|
|
564
|
+
' scenario: options.scenario,',
|
|
565
|
+
' sourceSurface: options.sourceSurface,',
|
|
566
|
+
' campaign: options.campaign,',
|
|
567
|
+
' packageSha256: options.packageSha256',
|
|
568
|
+
' })',
|
|
569
|
+
' });',
|
|
570
|
+
'}',
|
|
571
|
+
'',
|
|
572
|
+
'// Call at start/end of a single runtime invocation:',
|
|
573
|
+
'// await trackSkillEvent("skill_invoked", { installationId, invocationId, scenario });',
|
|
574
|
+
'// await trackSkillEvent("skill_completed", { installationId, invocationId, scenario });',
|
|
575
|
+
'// await trackSkillEvent("skill_failed", { installationId, invocationId, scenario });'
|
|
576
|
+
].join('\n');
|
|
577
|
+
if (flags.out) {
|
|
578
|
+
const outPath = path.resolve(flags.out);
|
|
579
|
+
await mkdir(path.dirname(outPath), { recursive: true });
|
|
580
|
+
await writeFile(outPath, `${snippet}\n`, { mode: 0o600 });
|
|
581
|
+
return { ok: true, outPath, snippet };
|
|
582
|
+
}
|
|
583
|
+
return snippet;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
async function submitTrackEvent(root, flags) {
|
|
587
|
+
const inspected = await inspectSkill(root, flags);
|
|
588
|
+
const registry = inspected.registry || {};
|
|
589
|
+
const publicToken = normalizeText(flags['public-token'] || registry.publicToken, 512);
|
|
590
|
+
const registryUrl = normalizeText(flags['registry-url'] || registry.registryUrl, 800) || DEFAULT_REGISTRY_URL;
|
|
591
|
+
const eventType = normalizeText(flags.event, 80);
|
|
592
|
+
if (!publicToken) {
|
|
593
|
+
throw new Error(`Missing public token. Run ${COMMAND_NAME} attach first or pass --public-token.`);
|
|
594
|
+
}
|
|
595
|
+
if (!eventType) {
|
|
596
|
+
throw new Error('Missing --event.');
|
|
597
|
+
}
|
|
598
|
+
const payload = {
|
|
599
|
+
publicToken,
|
|
600
|
+
eventType,
|
|
601
|
+
eventSource: normalizeText(flags['event-source'] || 'manual', 40),
|
|
602
|
+
runtimeKind: normalizeText(flags['runtime-kind'] || 'cli-test', 80),
|
|
603
|
+
hub: normalizeText(flags.hub || 'local-test', 80),
|
|
604
|
+
skillKey: inspected.skillKey,
|
|
605
|
+
installationId: normalizeText(flags['installation-id'] || 'local-test-install', 220),
|
|
606
|
+
invocationId: normalizeText(flags['invocation-id'] || `cli-${Date.now()}`, 160),
|
|
607
|
+
scenario: normalizeText(flags.scenario || 'local-test', 120),
|
|
608
|
+
sourceSurface: normalizeText(flags['source-surface'] || 'cli', 120),
|
|
609
|
+
campaign: normalizeText(flags.campaign, 120) || undefined,
|
|
610
|
+
platformSkillUrl: normalizeText(flags['platform-skill-url'], 1000) || undefined,
|
|
611
|
+
packageSha256: inspected.package.sha256,
|
|
612
|
+
idempotencyKey: normalizeText(flags.idempotencyKey || flags['idempotency-key'], 220) || undefined
|
|
613
|
+
};
|
|
614
|
+
if (flags.dryRun) {
|
|
615
|
+
return { ok: true, dryRun: true, registryUrl, payload };
|
|
616
|
+
}
|
|
617
|
+
const result = await postJson(registryUrl, payload, Number(flags['timeout-ms'] || 20_000));
|
|
618
|
+
return { ok: true, registryUrl, payload, result };
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
async function setupAgentWorkflow(root, flags) {
|
|
622
|
+
const hub = normalizeText(flags.hub || 'generic', 40).toLowerCase();
|
|
623
|
+
const absoluteRoot = path.resolve(root || '.');
|
|
624
|
+
const outDir = path.resolve(flags['out-dir'] || path.join(absoluteRoot, WORK_DIR));
|
|
625
|
+
await mkdir(outDir, { recursive: true });
|
|
626
|
+
const manifestResult = await writeManifest(absoluteRoot, {
|
|
627
|
+
...flags,
|
|
628
|
+
'local-manifest': true,
|
|
629
|
+
'manifest-path': flags['manifest-path'] || path.join(outDir, MANIFEST_FILE)
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
const promptPath = path.join(outDir, `upload-${safeSkillKey(hub)}.md`);
|
|
633
|
+
const promptResult = await writePrompt(absoluteRoot, { ...flags, hub, out: promptPath });
|
|
634
|
+
const snippetPath = path.join(outDir, 'runtime-events.js');
|
|
635
|
+
let snippetResult = null;
|
|
636
|
+
if (!flags['no-snippet']) {
|
|
637
|
+
snippetResult = await writeRuntimeSnippet(absoluteRoot, {
|
|
638
|
+
...flags,
|
|
639
|
+
target: flags.target || 'js',
|
|
640
|
+
out: snippetPath
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const warnings = [];
|
|
645
|
+
let promptEvent = null;
|
|
646
|
+
if (!flags['no-track']) {
|
|
647
|
+
try {
|
|
648
|
+
promptEvent = await submitTrackEvent(absoluteRoot, {
|
|
649
|
+
...flags,
|
|
650
|
+
hub,
|
|
651
|
+
event: 'upload_prompt_generated',
|
|
652
|
+
'event-source': 'cli',
|
|
653
|
+
'runtime-kind': 'agent-handoff',
|
|
654
|
+
scenario: flags.scenario || 'third-party-skill-upload'
|
|
655
|
+
});
|
|
656
|
+
} catch (error) {
|
|
657
|
+
warnings.push(`upload_prompt_generated was not recorded: ${error instanceof Error ? error.message : String(error)}`);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return {
|
|
662
|
+
ok: true,
|
|
663
|
+
hub,
|
|
664
|
+
manifestPath: manifestResult.manifestPath,
|
|
665
|
+
promptPath: typeof promptResult === 'string' ? promptPath : promptResult.outPath,
|
|
666
|
+
snippetPath: snippetResult ? snippetResult.outPath : null,
|
|
667
|
+
promptEvent,
|
|
668
|
+
warnings,
|
|
669
|
+
next: [
|
|
670
|
+
`Open ${path.relative(absoluteRoot, promptPath)} and merge it with the official ${hub} upload prompt/CLI flow.`,
|
|
671
|
+
`If ${hub} provides its own upload prompt or CLI, treat that official flow as authoritative.`,
|
|
672
|
+
'Do not upload secrets, .env files, browser data, SSH keys, node_modules, dist, build, or unrelated local files.',
|
|
673
|
+
`If the Skill is published, record the public URL with: ${COMMAND_NAME} track . --event hub_upload_succeeded --hub ${hub} --platform-skill-url <url>`,
|
|
674
|
+
snippetResult
|
|
675
|
+
? `If the runtime supports HTTP/MCP/webhook, adapt ${path.relative(absoluteRoot, snippetPath)} into the Skill runtime.`
|
|
676
|
+
: 'Runtime snippet was skipped.'
|
|
677
|
+
]
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
429
681
|
async function main() {
|
|
430
682
|
const [command = '', ...commandArgs] = process.argv.slice(2);
|
|
431
683
|
if (!command || command === '--help' || command === '-h') {
|
|
@@ -447,14 +699,37 @@ async function main() {
|
|
|
447
699
|
print(result, flags.json);
|
|
448
700
|
return;
|
|
449
701
|
}
|
|
702
|
+
if (command === 'setup' || command === 'handoff') {
|
|
703
|
+
const result = await setupAgentWorkflow(root, flags);
|
|
704
|
+
if (flags.json) {
|
|
705
|
+
print(result, true);
|
|
706
|
+
} else {
|
|
707
|
+
const lines = [
|
|
708
|
+
`Wrote ${result.manifestPath}`,
|
|
709
|
+
`Wrote ${result.promptPath}`,
|
|
710
|
+
result.snippetPath ? `Wrote ${result.snippetPath}` : null,
|
|
711
|
+
...result.warnings.map((warning) => `Warning: ${warning}`),
|
|
712
|
+
'',
|
|
713
|
+
'Next:',
|
|
714
|
+
...result.next.map((item) => `- ${item}`)
|
|
715
|
+
].filter(Boolean);
|
|
716
|
+
print(lines.join('\n'), false);
|
|
717
|
+
}
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
450
720
|
if (command === 'attach' || command === 'link') {
|
|
451
721
|
const result = await writeManifest(root, flags);
|
|
452
722
|
print(flags.json ? result : `Wrote ${result.manifestPath}`, flags.json);
|
|
453
723
|
return;
|
|
454
724
|
}
|
|
455
725
|
if (command === 'pack') {
|
|
456
|
-
const
|
|
457
|
-
|
|
726
|
+
const message = `${COMMAND_NAME} no longer creates upload packages. Run "${COMMAND_NAME} inspect ${root}" for a local source fingerprint, then ask your Agent to package and upload according to the target Skill hub rules.`;
|
|
727
|
+
if (flags.json) {
|
|
728
|
+
print({ ok: false, error: 'pack_disabled', message }, true);
|
|
729
|
+
process.exitCode = 2;
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
fail(message, 2);
|
|
458
733
|
return;
|
|
459
734
|
}
|
|
460
735
|
if (command === 'prompt') {
|
|
@@ -466,6 +741,20 @@ async function main() {
|
|
|
466
741
|
}
|
|
467
742
|
return;
|
|
468
743
|
}
|
|
744
|
+
if (command === 'snippet') {
|
|
745
|
+
const result = await writeRuntimeSnippet(root, flags);
|
|
746
|
+
if (typeof result === 'string' || flags.json) {
|
|
747
|
+
print(result, flags.json);
|
|
748
|
+
} else {
|
|
749
|
+
print(`Wrote ${result.outPath}`, false);
|
|
750
|
+
}
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
if (command === 'track') {
|
|
754
|
+
const result = await submitTrackEvent(root, flags);
|
|
755
|
+
print(result, flags.json);
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
469
758
|
fail(`Unknown command: ${command}\n\n${usage()}`, 2);
|
|
470
759
|
} catch (error) {
|
|
471
760
|
fail(error instanceof Error ? error.message : String(error || 'xiashe-skill failed'), 1);
|