@xiashe/skill 0.1.0 → 0.1.2
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 +451 -61
- 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
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { createHash } from 'node:crypto';
|
|
3
|
+
import { createHash, createHmac } 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.2';
|
|
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');
|
|
@@ -19,6 +19,9 @@ const MANIFEST_FILE = process.env.XIASHE_SKILL_MANIFEST_FILE || (COMMAND_NAME ==
|
|
|
19
19
|
const WORK_DIR = process.env.XIASHE_SKILL_WORK_DIR || (COMMAND_NAME === 'agentpie-skill' ? '.agentpie' : '.xiashe');
|
|
20
20
|
const DEFAULT_REGISTRY_URL = process.env.XIASHE_SKILL_REGISTRY_URL || `${DEFAULT_BASE_URL}/registry/skill-events`;
|
|
21
21
|
const DEFAULT_CLAIM_URL = process.env.XIASHE_SKILL_CLAIM_URL || `${DEFAULT_BASE_URL}/registry/skill/claim`;
|
|
22
|
+
const EVENT_SCHEMA_VERSION = process.env.XIASHE_SKILL_EVENT_SCHEMA_VERSION || (REGISTRY_PROVIDER === 'agentpie'
|
|
23
|
+
? 'agentpie.skill.event.v1'
|
|
24
|
+
: 'xiashe.skill.event.v1');
|
|
22
25
|
const DEFAULT_IGNORE = new Set([
|
|
23
26
|
'.git',
|
|
24
27
|
'.xiashe',
|
|
@@ -34,17 +37,20 @@ const DEFAULT_IGNORE = new Set([
|
|
|
34
37
|
'coverage'
|
|
35
38
|
]);
|
|
36
39
|
const MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
37
|
-
const
|
|
40
|
+
const MAX_SOURCE_BYTES = 30 * 1024 * 1024;
|
|
38
41
|
|
|
39
42
|
function usage() {
|
|
40
43
|
return `${COMMAND_NAME} ${VERSION}
|
|
41
44
|
|
|
42
45
|
Usage:
|
|
43
46
|
${COMMAND_NAME} inspect [skill-dir] [--json]
|
|
47
|
+
${COMMAND_NAME} setup [skill-dir] --code <dynamic-code> --hub <red|clawhub|skillhub|claude|dify|generic>
|
|
48
|
+
${COMMAND_NAME} setup [skill-dir] --public-token <token> --skill-id <id> --hub <red|clawhub|skillhub|claude|dify|generic>
|
|
44
49
|
${COMMAND_NAME} attach [skill-dir] --code <dynamic-code> [--name <name>]
|
|
45
50
|
${COMMAND_NAME} attach [skill-dir] --public-token <token> [--skill-id <id>] [--name <name>]
|
|
46
|
-
${COMMAND_NAME}
|
|
47
|
-
${COMMAND_NAME}
|
|
51
|
+
${COMMAND_NAME} prompt [skill-dir] --hub <red|clawhub|skillhub|claude|dify|generic> [--source-url <url>] [--out <file>]
|
|
52
|
+
${COMMAND_NAME} snippet [skill-dir] [--target js|curl] [--out <file>]
|
|
53
|
+
${COMMAND_NAME} track [skill-dir] --event <event> [--hub <hub>] [--installation-id <id>] [--invocation-id <id>]
|
|
48
54
|
|
|
49
55
|
Options:
|
|
50
56
|
--public-token <token> Public write token issued by ${PRODUCT_NAME} registry.
|
|
@@ -54,10 +60,24 @@ Options:
|
|
|
54
60
|
--registry-url <url> Event endpoint written to the manifest.
|
|
55
61
|
--name <name> Skill display name override.
|
|
56
62
|
--description <text> Skill description override.
|
|
57
|
-
--hub <
|
|
58
|
-
--
|
|
59
|
-
--
|
|
60
|
-
--
|
|
63
|
+
--hub <hub> Target Skill hub prompt style: red, clawhub, skillhub, claude, dify, or generic.
|
|
64
|
+
--event <event> Runtime event to submit for testing.
|
|
65
|
+
--installation-id <id> Stable anonymous install id for runtime analytics.
|
|
66
|
+
--invocation-id <id> Unique invocation id for one skill call.
|
|
67
|
+
--scenario <label> Short scenario label.
|
|
68
|
+
--campaign <label> Campaign or promotion label.
|
|
69
|
+
--platform-skill-url <url> Published Skill URL for hub_upload_succeeded events.
|
|
70
|
+
--platform-prompt-file <p> Official third-party hub prompt file to embed in the handoff.
|
|
71
|
+
--platform-command <cmd> Official third-party hub CLI/upload command to include.
|
|
72
|
+
--manifest-path <path> Registry manifest output path. Defaults to ${MANIFEST_FILE} for attach, ${WORK_DIR}/${MANIFEST_FILE} for setup.
|
|
73
|
+
--signing-secret <secret> Optional HMAC secret for signed runtime events.
|
|
74
|
+
--source-url <url> User-provided source URL an Agent can use during third-party upload.
|
|
75
|
+
--package-url <url> Deprecated alias for --source-url.
|
|
76
|
+
--out <path> Output prompt or snippet file.
|
|
77
|
+
--out-dir <path> Output directory for setup artifacts. Defaults to ${WORK_DIR}/.
|
|
78
|
+
--no-snippet Setup should skip writing the runtime analytics snippet.
|
|
79
|
+
--no-track Setup should skip sending upload_prompt_generated.
|
|
80
|
+
--dry-run Print planned event payloads without network writes.
|
|
61
81
|
--json Print machine-readable JSON.
|
|
62
82
|
--help Show help.
|
|
63
83
|
--version Show version.
|
|
@@ -71,6 +91,7 @@ function fail(message, code = 1) {
|
|
|
71
91
|
|
|
72
92
|
function parseArgs(argv) {
|
|
73
93
|
const args = { _: [], json: false, dryRun: false };
|
|
94
|
+
const booleanFlags = new Set(['no-snippet', 'no-track']);
|
|
74
95
|
for (let i = 0; i < argv.length; i += 1) {
|
|
75
96
|
const value = argv[i];
|
|
76
97
|
if (value === '--help' || value === '-h') {
|
|
@@ -91,6 +112,10 @@ function parseArgs(argv) {
|
|
|
91
112
|
}
|
|
92
113
|
if (value.startsWith('--')) {
|
|
93
114
|
const key = value.slice(2);
|
|
115
|
+
if (booleanFlags.has(key)) {
|
|
116
|
+
args[key] = true;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
94
119
|
const next = argv[i + 1];
|
|
95
120
|
if (!next || next.startsWith('--')) {
|
|
96
121
|
fail(`Missing value for ${value}.`, 2);
|
|
@@ -123,6 +148,26 @@ function safeSkillKey(value) {
|
|
|
123
148
|
.replace(/^-+|-+$/g, '') || 'skill';
|
|
124
149
|
}
|
|
125
150
|
|
|
151
|
+
function canonicalSignaturePayload(payload) {
|
|
152
|
+
return [
|
|
153
|
+
payload.schemaVersion || EVENT_SCHEMA_VERSION,
|
|
154
|
+
payload.eventType || '',
|
|
155
|
+
payload.idempotencyKey || '',
|
|
156
|
+
Number.isFinite(payload.occurredAt) ? String(Math.floor(payload.occurredAt)) : '',
|
|
157
|
+
payload.installationId || '',
|
|
158
|
+
payload.invocationId || '',
|
|
159
|
+
payload.hub || '',
|
|
160
|
+
payload.skillKey || ''
|
|
161
|
+
].join('\n');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function signEventPayload(payload, signingSecret) {
|
|
165
|
+
if (!signingSecret) return undefined;
|
|
166
|
+
return createHmac('sha256', signingSecret)
|
|
167
|
+
.update(canonicalSignaturePayload(payload))
|
|
168
|
+
.digest('hex');
|
|
169
|
+
}
|
|
170
|
+
|
|
126
171
|
async function readJsonFile(filePath) {
|
|
127
172
|
if (!existsSync(filePath)) return null;
|
|
128
173
|
try {
|
|
@@ -133,7 +178,11 @@ async function readJsonFile(filePath) {
|
|
|
133
178
|
}
|
|
134
179
|
|
|
135
180
|
async function inferSkillMetadata(root, flags = {}) {
|
|
136
|
-
const
|
|
181
|
+
const manifestPath = flags['manifest-path']
|
|
182
|
+
? path.resolve(flags['manifest-path'])
|
|
183
|
+
: path.join(root, MANIFEST_FILE);
|
|
184
|
+
const localManifestPath = path.join(root, WORK_DIR, MANIFEST_FILE);
|
|
185
|
+
const manifest = await readJsonFile(manifestPath) || await readJsonFile(localManifestPath);
|
|
137
186
|
const packageJson = await readJsonFile(path.join(root, 'package.json'));
|
|
138
187
|
const skillMdPath = path.join(root, 'SKILL.md');
|
|
139
188
|
const readmePath = path.join(root, 'README.md');
|
|
@@ -235,7 +284,7 @@ async function inspectSkill(root, flags = {}) {
|
|
|
235
284
|
fileCount: files.length,
|
|
236
285
|
totalBytes: digest.totalBytes,
|
|
237
286
|
sha256: digest.sha256,
|
|
238
|
-
tooLarge: digest.totalBytes >
|
|
287
|
+
tooLarge: digest.totalBytes > MAX_SOURCE_BYTES,
|
|
239
288
|
files: files.map((file) => ({ path: file.relative, size: file.size }))
|
|
240
289
|
}
|
|
241
290
|
};
|
|
@@ -282,10 +331,14 @@ async function writeManifest(root, flags) {
|
|
|
282
331
|
skillId: normalizeText(flags['skill-id'] || claim?.skillId, 160) || null,
|
|
283
332
|
publicToken,
|
|
284
333
|
registryUrl: normalizeText(flags['registry-url'] || claim?.registryUrl, 800) || DEFAULT_REGISTRY_URL,
|
|
334
|
+
eventSchemaVersion: EVENT_SCHEMA_VERSION,
|
|
335
|
+
signing: {
|
|
336
|
+
mode: normalizeText(flags['signing-secret'], 20) ? 'optional' : 'off',
|
|
337
|
+
secretEnv: `${REGISTRY_PROVIDER.toUpperCase()}_SKILL_SIGNING_SECRET`
|
|
338
|
+
},
|
|
285
339
|
analytics: {
|
|
286
340
|
enabled: true,
|
|
287
341
|
allowedEvents: [
|
|
288
|
-
'package_created',
|
|
289
342
|
'upload_prompt_generated',
|
|
290
343
|
'hub_upload_attempted',
|
|
291
344
|
'hub_upload_succeeded',
|
|
@@ -298,67 +351,132 @@ async function writeManifest(root, flags) {
|
|
|
298
351
|
]
|
|
299
352
|
}
|
|
300
353
|
},
|
|
301
|
-
|
|
354
|
+
sourceFingerprint: {
|
|
302
355
|
sha256: inspected.package.sha256,
|
|
303
356
|
fileCount: inspected.package.fileCount,
|
|
304
357
|
totalBytes: inspected.package.totalBytes,
|
|
305
358
|
generatedAt: now
|
|
306
359
|
}
|
|
307
360
|
};
|
|
308
|
-
const manifestPath = path
|
|
361
|
+
const manifestPath = flags['manifest-path']
|
|
362
|
+
? path.resolve(flags['manifest-path'])
|
|
363
|
+
: flags['local-manifest']
|
|
364
|
+
? path.join(inspected.root, WORK_DIR, MANIFEST_FILE)
|
|
365
|
+
: path.join(inspected.root, MANIFEST_FILE);
|
|
366
|
+
await mkdir(path.dirname(manifestPath), { recursive: true });
|
|
309
367
|
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
|
|
310
368
|
return { ok: true, manifestPath, manifest };
|
|
311
369
|
}
|
|
312
370
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
throw new Error(`Package is too large: ${inspected.package.totalBytes} bytes. Max ${MAX_PACKAGE_BYTES}.`);
|
|
371
|
+
function hubInstruction(hub) {
|
|
372
|
+
if (hub === 'red') {
|
|
373
|
+
return 'Red Skill 的官方上传 prompt / 页面流程是主流程。请先从小红书创作服务平台获取或使用用户提供的 Red Skill 上传 prompt,再把下面的虾舍 registry 要求作为附加说明合并进去。';
|
|
317
374
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
return { ok: true, dryRun: true, outDir, ...inspected };
|
|
375
|
+
if (hub === 'clawhub') {
|
|
376
|
+
return 'ClawHub 的官方 CLI、prompt 或页面流程是主流程。请先确认 ClawHub 当前要求,再把下面的虾舍 registry 要求作为附加说明合并进去。';
|
|
321
377
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
378
|
+
if (hub === 'skillhub') {
|
|
379
|
+
return 'SkillHub 的官方 CLI、prompt 或页面流程是主流程。请先确认 SkillHub 当前要求,再把下面的虾舍 registry 要求作为附加说明合并进去。';
|
|
380
|
+
}
|
|
381
|
+
if (hub === 'claude') {
|
|
382
|
+
return 'Claude Skills 的官方上传/导入流程是主流程。请先确认 Claude 当前 Skill 格式和共享要求,再把下面的 registry 披露文本放入允许的说明文件。';
|
|
383
|
+
}
|
|
384
|
+
if (hub === 'dify') {
|
|
385
|
+
return 'Dify Plugin/Tool 的官方打包和发布流程是主流程。请先按 Dify 插件规范处理 manifest 和工具定义,再把下面的 registry 披露文本放入允许的 README/说明字段。';
|
|
386
|
+
}
|
|
387
|
+
return '目标 Skill Hub 的官方 CLI、prompt 或页面流程是主流程。请先确认平台当前要求,再把下面的虾舍 registry 要求作为附加说明合并进去。';
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function platformPromptPlaceholder(hub) {
|
|
391
|
+
if (hub === 'red') {
|
|
392
|
+
return '如果用户还没有提供 Red Skill 官方上传 prompt,请先请用户从小红书创作服务平台复制该 prompt;不要用虾舍 prompt 替代 Red Skill 官方 prompt。';
|
|
393
|
+
}
|
|
394
|
+
if (hub === 'clawhub') {
|
|
395
|
+
return '如果 ClawHub 提供官方 CLI 命令或上传 prompt,请优先使用官方命令/prompt;不要用虾舍 prompt 替代 ClawHub 官方流程。';
|
|
396
|
+
}
|
|
397
|
+
if (hub === 'skillhub') {
|
|
398
|
+
return '如果 SkillHub 提供官方 CLI 命令或上传 prompt,请优先使用官方命令/prompt;不要用虾舍 prompt 替代 SkillHub 官方流程。';
|
|
399
|
+
}
|
|
400
|
+
if (hub === 'claude') {
|
|
401
|
+
return '如果 Claude 提供官方 Skill 导入/上传说明,请优先使用官方说明;不要用虾舍 prompt 替代 Claude 官方流程。';
|
|
402
|
+
}
|
|
403
|
+
if (hub === 'dify') {
|
|
404
|
+
return '如果 Dify 提供官方插件打包/发布命令,请优先使用官方命令;不要用虾舍 prompt 替代 Dify 官方流程。';
|
|
405
|
+
}
|
|
406
|
+
return '如果目标平台提供官方 CLI 命令或上传 prompt,请优先使用官方命令/prompt;不要用虾舍 prompt 替代平台官方流程。';
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function uploadCompatibilityLines(hub, manifestFile) {
|
|
410
|
+
const localManifest = `${WORK_DIR}/${manifestFile}`;
|
|
411
|
+
if (hub === 'red') {
|
|
412
|
+
return [
|
|
413
|
+
'Red Skill 兼容性要求:',
|
|
414
|
+
'- Red Skill 如果只接受 Markdown/TXT,请不要把 JSON manifest 当作源码文件上传。',
|
|
415
|
+
`- ${localManifest} 保留在用户本地用于虾舍 registry claim 和 analytics;只有在 Red Skill 官方流程明确允许附加 JSON 文件时才上传。`,
|
|
416
|
+
'- 如需在 Red Skill 页面披露 registry,请把下面的“公开披露文本”写入 Skill 介绍、README 或平台允许的 Markdown/TXT 字段。'
|
|
417
|
+
];
|
|
418
|
+
}
|
|
419
|
+
return [
|
|
420
|
+
'第三方平台兼容性要求:',
|
|
421
|
+
`- 如果目标平台不接受 ${localManifest} 这类 JSON manifest,请不要强行上传该文件。`,
|
|
422
|
+
'- 保留本地 registry manifest,并把“公开披露文本”写入平台允许的说明字段、README、SKILL.md 或 Markdown/TXT 附件。',
|
|
423
|
+
'- 如果平台支持额外 manifest / metadata 文件,可以在提交前向用户确认是否一并上传。'
|
|
424
|
+
];
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function publicDisclosure(inspected, registryUrl) {
|
|
428
|
+
return [
|
|
429
|
+
'公开披露文本:',
|
|
430
|
+
'```markdown',
|
|
431
|
+
`### ${PRODUCT_NAME} Registry / Analytics`,
|
|
432
|
+
'',
|
|
433
|
+
`This Skill is registered by its creator with ${PRODUCT_NAME} for public attribution and aggregated analytics.`,
|
|
434
|
+
'',
|
|
435
|
+
`- Registry provider: ${REGISTRY_PROVIDER}`,
|
|
436
|
+
`- Skill key: ${inspected.skillKey}`,
|
|
437
|
+
`- Registry event endpoint: ${registryUrl}`,
|
|
438
|
+
'- Events, if enabled by the runtime/platform, should contain only public metadata and aggregate status.',
|
|
439
|
+
'- Do not send user prompts, private files, credentials, environment variables, account tokens, or raw business content.',
|
|
440
|
+
'- Analytics can be removed by deleting the registry manifest/snippet or disabling it in the creator dashboard.',
|
|
441
|
+
'```'
|
|
442
|
+
];
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async function readPlatformPrompt(flags) {
|
|
446
|
+
const promptPath = normalizeText(flags['platform-prompt-file'], 1000);
|
|
447
|
+
if (!promptPath) return '';
|
|
448
|
+
const absolute = path.resolve(promptPath);
|
|
449
|
+
const info = await stat(absolute).catch(() => null);
|
|
450
|
+
if (!info?.isFile()) {
|
|
451
|
+
throw new Error(`Platform prompt file not found: ${absolute}`);
|
|
452
|
+
}
|
|
453
|
+
if (info.size > MAX_FILE_BYTES) {
|
|
454
|
+
throw new Error(`Platform prompt file is too large: ${absolute}`);
|
|
455
|
+
}
|
|
456
|
+
return readFile(absolute, 'utf8');
|
|
344
457
|
}
|
|
345
458
|
|
|
346
|
-
function uploadPrompt(inspected, flags) {
|
|
459
|
+
async function uploadPrompt(inspected, flags) {
|
|
347
460
|
const hub = normalizeText(flags.hub || 'generic', 40).toLowerCase();
|
|
348
|
-
const
|
|
461
|
+
const sourceUrl = normalizeText(flags['source-url'] || flags['package-url'], 1000);
|
|
462
|
+
const platformCommand = normalizeText(flags['platform-command'], 1000);
|
|
463
|
+
const platformPrompt = await readPlatformPrompt(flags);
|
|
349
464
|
const registry = inspected.registry || {};
|
|
350
465
|
const registryUrl = registry.registryUrl || DEFAULT_REGISTRY_URL;
|
|
351
466
|
const eventPayload = {
|
|
352
467
|
publicToken: registry.publicToken || '<xiashe public token from xiashe.skill.json>',
|
|
468
|
+
schemaVersion: registry.eventSchemaVersion || EVENT_SCHEMA_VERSION,
|
|
353
469
|
eventType: 'hub_upload_succeeded',
|
|
354
470
|
hub,
|
|
355
471
|
skillKey: inspected.skillKey,
|
|
472
|
+
idempotencyKey: '<hub-upload-succeeded-stable-key>',
|
|
356
473
|
packageSha256: inspected.package.sha256,
|
|
357
474
|
platformSkillUrl: '<published skill url>',
|
|
358
475
|
scenario: '<short usage scenario if the hub asks for one>'
|
|
359
476
|
};
|
|
360
477
|
const runtimePayload = {
|
|
361
478
|
publicToken: registry.publicToken || '<public token from registry manifest>',
|
|
479
|
+
schemaVersion: registry.eventSchemaVersion || EVENT_SCHEMA_VERSION,
|
|
362
480
|
eventType: 'skill_invoked',
|
|
363
481
|
eventSource: 'runtime',
|
|
364
482
|
runtimeKind: '<mcp|api|agent|webhook|platform>',
|
|
@@ -366,29 +484,52 @@ function uploadPrompt(inspected, flags) {
|
|
|
366
484
|
skillKey: inspected.skillKey,
|
|
367
485
|
installationId: '<stable anonymous install id>',
|
|
368
486
|
invocationId: '<unique invocation id>',
|
|
369
|
-
|
|
487
|
+
idempotencyKey: '<unique invocation id>:skill_invoked',
|
|
488
|
+
scenario: '<short usage scenario label only>',
|
|
489
|
+
sourceSurface: '<profile|hub|agent|direct>',
|
|
490
|
+
campaign: '<optional promotion label>'
|
|
370
491
|
};
|
|
371
|
-
const hubLine = hub
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
492
|
+
const hubLine = hubInstruction(hub);
|
|
493
|
+
const sourceLine = sourceUrl
|
|
494
|
+
? `你可以使用用户明确提供的源码或仓库 URL:${sourceUrl}`
|
|
495
|
+
: `请在用户当前 Skill 项目根目录内操作;最终上传形态必须由你按目标 Skill 平台规则生成。`;
|
|
496
|
+
const officialFlowLines = [
|
|
497
|
+
'第三方平台官方流程:',
|
|
498
|
+
`- ${hubLine}`,
|
|
499
|
+
'- 平台官方 prompt / CLI / 页面表单是上传依据;虾舍只提供 registry 和 analytics 附加要求。',
|
|
500
|
+
'- 如果平台官方要求与虾舍附加说明冲突,以平台官方上传要求为准;不要为了 analytics 绕过平台规则。',
|
|
501
|
+
platformCommand ? `- 用户提供的官方命令:${platformCommand}` : `- ${platformPromptPlaceholder(hub)}`
|
|
502
|
+
];
|
|
503
|
+
if (platformPrompt) {
|
|
504
|
+
officialFlowLines.push('', '用户提供的第三方平台官方 prompt:', '```text', platformPrompt.trim(), '```');
|
|
505
|
+
}
|
|
506
|
+
const compatibilityLines = uploadCompatibilityLines(hub, MANIFEST_FILE);
|
|
507
|
+
const disclosureLines = publicDisclosure(inspected, registryUrl);
|
|
377
508
|
return [
|
|
378
|
-
|
|
509
|
+
`你现在要帮用户把 Skill 接入 ${PRODUCT_NAME} registry,并按第三方平台官方流程发布:${inspected.name}`,
|
|
510
|
+
'',
|
|
511
|
+
...officialFlowLines,
|
|
379
512
|
'',
|
|
380
|
-
hubLine,
|
|
381
513
|
sourceLine,
|
|
382
514
|
'',
|
|
383
|
-
|
|
515
|
+
`${PRODUCT_NAME} 职责边界:`,
|
|
516
|
+
`- ${PRODUCT_NAME} 只负责 registry manifest、公开归因和 analytics 回传说明,不负责构建、打包、上传或审核这个 Skill。`,
|
|
517
|
+
'- 最终上传文件、目录结构、压缩包、表单字段和上线版本,必须由你按照目标 Skill 平台的官方要求处理。',
|
|
518
|
+
'- 如果目标平台要求重新整理源码、生成 markdown、压缩包或填写表单,请你在用户授权范围内完成,并在提交前向用户展示将上传的文件和字段。',
|
|
519
|
+
'',
|
|
520
|
+
...compatibilityLines,
|
|
521
|
+
'',
|
|
522
|
+
...disclosureLines,
|
|
523
|
+
'',
|
|
524
|
+
`${PRODUCT_NAME} registry 附加要求:`,
|
|
384
525
|
`- Skill 名称:${inspected.name}`,
|
|
385
526
|
`- Skill Key:${inspected.skillKey}`,
|
|
386
527
|
`- 版本:${inspected.version}`,
|
|
387
528
|
`- 描述:${inspected.description || '<从 SKILL.md/README.md 中提炼>'}`,
|
|
388
|
-
`-
|
|
529
|
+
`- 源码指纹 SHA256:${inspected.package.sha256}`,
|
|
389
530
|
'- 不要读取或上传 .env、密钥、浏览器数据、SSH key、系统目录或 node_modules。',
|
|
390
531
|
'- 不要新增 postinstall hook、后台常驻进程、隐藏脚本或混淆逻辑。',
|
|
391
|
-
`- 如果平台要求安全说明,请明确说明这是用户显式发布的 Skill
|
|
532
|
+
`- 如果平台要求安全说明,请明确说明这是用户显式发布的 Skill 项目,${MANIFEST_FILE} 只包含公开 registry 归因和可关闭的 analytics 声明。`,
|
|
392
533
|
`- 如果目标平台支持工具调用、MCP server、webhook 或 Skill runtime HTTP 请求,请在 Skill 被调用时向 ${PRODUCT_NAME} registry 发送聚合事件:skill_invoked / skill_completed / skill_failed。`,
|
|
393
534
|
'- 事件里只放公开元数据、聚合状态和简短场景标签,不要发送用户输入原文、私密业务内容、账号凭据或文件内容。',
|
|
394
535
|
'- installationId 必须是稳定的匿名实例 ID,用于统计唯一安装实例;不要使用邮箱、手机号、真实用户名或设备序列号。',
|
|
@@ -416,7 +557,7 @@ async function writePrompt(root, flags) {
|
|
|
416
557
|
throw new Error('Missing --hub <red|generic>.');
|
|
417
558
|
}
|
|
418
559
|
const inspected = await inspectSkill(root, flags);
|
|
419
|
-
const prompt = uploadPrompt(inspected, flags);
|
|
560
|
+
const prompt = await uploadPrompt(inspected, flags);
|
|
420
561
|
if (flags.out) {
|
|
421
562
|
const outPath = path.resolve(flags.out);
|
|
422
563
|
await mkdir(path.dirname(outPath), { recursive: true });
|
|
@@ -426,6 +567,217 @@ async function writePrompt(root, flags) {
|
|
|
426
567
|
return prompt;
|
|
427
568
|
}
|
|
428
569
|
|
|
570
|
+
async function writeRuntimeSnippet(root, flags) {
|
|
571
|
+
const inspected = await inspectSkill(root, flags);
|
|
572
|
+
const registry = inspected.registry || {};
|
|
573
|
+
const registryUrl = registry.registryUrl || DEFAULT_REGISTRY_URL;
|
|
574
|
+
const publicToken = registry.publicToken || '<public token from registry manifest>';
|
|
575
|
+
const eventSchemaVersion = registry.eventSchemaVersion || EVENT_SCHEMA_VERSION;
|
|
576
|
+
const target = normalizeText(flags.target || 'js', 20).toLowerCase();
|
|
577
|
+
const snippet = target === 'curl'
|
|
578
|
+
? [
|
|
579
|
+
`curl -X POST '${registryUrl}' \\`,
|
|
580
|
+
" -H 'content-type: application/json' \\",
|
|
581
|
+
' -d ' + JSON.stringify(JSON.stringify({
|
|
582
|
+
publicToken,
|
|
583
|
+
schemaVersion: eventSchemaVersion,
|
|
584
|
+
eventType: 'skill_invoked',
|
|
585
|
+
eventSource: 'runtime',
|
|
586
|
+
runtimeKind: 'mcp',
|
|
587
|
+
hub: '<hub>',
|
|
588
|
+
skillKey: inspected.skillKey,
|
|
589
|
+
installationId: '<anonymous-stable-install-id>',
|
|
590
|
+
invocationId: '<unique-call-id>',
|
|
591
|
+
idempotencyKey: '<unique-call-id>:skill_invoked',
|
|
592
|
+
scenario: '<short-scenario-label>'
|
|
593
|
+
}))
|
|
594
|
+
].join('\n')
|
|
595
|
+
: [
|
|
596
|
+
`const XIASHE_SKILL_REGISTRY_URL = ${JSON.stringify(registryUrl)};`,
|
|
597
|
+
`const XIASHE_SKILL_PUBLIC_TOKEN = ${JSON.stringify(publicToken)};`,
|
|
598
|
+
`const XIASHE_SKILL_EVENT_SCHEMA_VERSION = ${JSON.stringify(eventSchemaVersion)};`,
|
|
599
|
+
`const XIASHE_SKILL_SIGNING_SECRET = ${JSON.stringify(normalizeText(flags['signing-secret'], 512))} || globalThis.process?.env?.XIASHE_SKILL_SIGNING_SECRET || globalThis.process?.env?.AGENTPIE_SKILL_SIGNING_SECRET || '';`,
|
|
600
|
+
'',
|
|
601
|
+
'async function hmacSha256(secret, message) {',
|
|
602
|
+
' if (!secret || !globalThis.crypto?.subtle) return undefined;',
|
|
603
|
+
" const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);",
|
|
604
|
+
' const signature = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(message));',
|
|
605
|
+
" return Array.from(new Uint8Array(signature)).map((byte) => byte.toString(16).padStart(2, '0')).join('');",
|
|
606
|
+
'}',
|
|
607
|
+
'',
|
|
608
|
+
'function canonicalSignaturePayload(payload) {',
|
|
609
|
+
' return [',
|
|
610
|
+
' payload.schemaVersion || XIASHE_SKILL_EVENT_SCHEMA_VERSION,',
|
|
611
|
+
" payload.eventType || '',",
|
|
612
|
+
" payload.idempotencyKey || '',",
|
|
613
|
+
" Number.isFinite(payload.occurredAt) ? String(Math.floor(payload.occurredAt)) : '',",
|
|
614
|
+
" payload.installationId || '',",
|
|
615
|
+
" payload.invocationId || '',",
|
|
616
|
+
" payload.hub || '',",
|
|
617
|
+
" payload.skillKey || ''",
|
|
618
|
+
" ].join('\\n');",
|
|
619
|
+
'}',
|
|
620
|
+
'',
|
|
621
|
+
'export async function trackSkillEvent(eventType, options = {}) {',
|
|
622
|
+
' const occurredAt = options.occurredAt || Date.now();',
|
|
623
|
+
' const invocationId = options.invocationId;',
|
|
624
|
+
' const payload = {',
|
|
625
|
+
' publicToken: XIASHE_SKILL_PUBLIC_TOKEN,',
|
|
626
|
+
' schemaVersion: XIASHE_SKILL_EVENT_SCHEMA_VERSION,',
|
|
627
|
+
' eventType,',
|
|
628
|
+
" eventSource: options.eventSource || 'runtime',",
|
|
629
|
+
" runtimeKind: options.runtimeKind || 'mcp',",
|
|
630
|
+
` skillKey: ${JSON.stringify(inspected.skillKey)},`,
|
|
631
|
+
' hub: options.hub,',
|
|
632
|
+
' installationId: options.installationId,',
|
|
633
|
+
' invocationId,',
|
|
634
|
+
' idempotencyKey: options.idempotencyKey || `${invocationId || options.installationId || "runtime"}:${eventType}:${occurredAt}`,',
|
|
635
|
+
' scenario: options.scenario,',
|
|
636
|
+
' sourceSurface: options.sourceSurface,',
|
|
637
|
+
' campaign: options.campaign,',
|
|
638
|
+
' packageSha256: options.packageSha256,',
|
|
639
|
+
' occurredAt',
|
|
640
|
+
' };',
|
|
641
|
+
' const signature = await hmacSha256(XIASHE_SKILL_SIGNING_SECRET, canonicalSignaturePayload(payload));',
|
|
642
|
+
' if (signature) payload.signature = signature;',
|
|
643
|
+
' await fetch(XIASHE_SKILL_REGISTRY_URL, {',
|
|
644
|
+
" method: 'POST',",
|
|
645
|
+
" headers: { 'content-type': 'application/json' },",
|
|
646
|
+
' body: JSON.stringify(payload)',
|
|
647
|
+
' });',
|
|
648
|
+
'}',
|
|
649
|
+
'',
|
|
650
|
+
'// Call at start/end of a single runtime invocation:',
|
|
651
|
+
'// await trackSkillEvent("skill_invoked", { installationId, invocationId, scenario });',
|
|
652
|
+
'// await trackSkillEvent("skill_completed", { installationId, invocationId, scenario });',
|
|
653
|
+
'// await trackSkillEvent("skill_failed", { installationId, invocationId, scenario });'
|
|
654
|
+
].join('\n');
|
|
655
|
+
if (flags.out) {
|
|
656
|
+
const outPath = path.resolve(flags.out);
|
|
657
|
+
await mkdir(path.dirname(outPath), { recursive: true });
|
|
658
|
+
await writeFile(outPath, `${snippet}\n`, { mode: 0o600 });
|
|
659
|
+
return { ok: true, outPath, snippet };
|
|
660
|
+
}
|
|
661
|
+
return snippet;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
async function submitTrackEvent(root, flags) {
|
|
665
|
+
const inspected = await inspectSkill(root, flags);
|
|
666
|
+
const registry = inspected.registry || {};
|
|
667
|
+
const publicToken = normalizeText(flags['public-token'] || registry.publicToken, 512);
|
|
668
|
+
const registryUrl = normalizeText(flags['registry-url'] || registry.registryUrl, 800) || DEFAULT_REGISTRY_URL;
|
|
669
|
+
const eventType = normalizeText(flags.event, 80);
|
|
670
|
+
if (!publicToken) {
|
|
671
|
+
throw new Error(`Missing public token. Run ${COMMAND_NAME} attach first or pass --public-token.`);
|
|
672
|
+
}
|
|
673
|
+
if (!eventType) {
|
|
674
|
+
throw new Error('Missing --event.');
|
|
675
|
+
}
|
|
676
|
+
const occurredAt = Number(flags['occurred-at'] || Date.now());
|
|
677
|
+
const invocationId = normalizeText(flags['invocation-id'] || `cli-${Date.now()}`, 160);
|
|
678
|
+
const installationId = normalizeText(flags['installation-id'] || 'local-test-install', 220);
|
|
679
|
+
const hub = normalizeText(flags.hub || 'local-test', 80);
|
|
680
|
+
const payload = {
|
|
681
|
+
publicToken,
|
|
682
|
+
schemaVersion: normalizeText(flags['schema-version'], 80) || EVENT_SCHEMA_VERSION,
|
|
683
|
+
eventType,
|
|
684
|
+
eventSource: normalizeText(flags['event-source'] || 'manual', 40),
|
|
685
|
+
runtimeKind: normalizeText(flags['runtime-kind'] || 'cli-test', 80),
|
|
686
|
+
hub,
|
|
687
|
+
skillKey: inspected.skillKey,
|
|
688
|
+
installationId,
|
|
689
|
+
invocationId,
|
|
690
|
+
scenario: normalizeText(flags.scenario || 'local-test', 120),
|
|
691
|
+
sourceSurface: normalizeText(flags['source-surface'] || 'cli', 120),
|
|
692
|
+
campaign: normalizeText(flags.campaign, 120) || undefined,
|
|
693
|
+
platformSkillUrl: normalizeText(flags['platform-skill-url'], 1000) || undefined,
|
|
694
|
+
packageSha256: inspected.package.sha256,
|
|
695
|
+
idempotencyKey: normalizeText(flags.idempotencyKey || flags['idempotency-key'], 220) || `${invocationId}:${eventType}`,
|
|
696
|
+
occurredAt: Number.isFinite(occurredAt) ? occurredAt : Date.now()
|
|
697
|
+
};
|
|
698
|
+
const signingSecret = normalizeText(flags['signing-secret'], 512);
|
|
699
|
+
const signature = signEventPayload(payload, signingSecret);
|
|
700
|
+
if (signature) {
|
|
701
|
+
payload.signature = signature;
|
|
702
|
+
}
|
|
703
|
+
if (flags.dryRun) {
|
|
704
|
+
return { ok: true, dryRun: true, registryUrl, payload };
|
|
705
|
+
}
|
|
706
|
+
const result = await postJson(registryUrl, payload, Number(flags['timeout-ms'] || 20_000));
|
|
707
|
+
return { ok: true, registryUrl, payload, result };
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
async function setupAgentWorkflow(root, flags) {
|
|
711
|
+
const hub = normalizeText(flags.hub || 'generic', 40).toLowerCase();
|
|
712
|
+
const absoluteRoot = path.resolve(root || '.');
|
|
713
|
+
const outDir = path.resolve(flags['out-dir'] || path.join(absoluteRoot, WORK_DIR));
|
|
714
|
+
await mkdir(outDir, { recursive: true });
|
|
715
|
+
const manifestResult = await writeManifest(absoluteRoot, {
|
|
716
|
+
...flags,
|
|
717
|
+
'local-manifest': true,
|
|
718
|
+
'manifest-path': flags['manifest-path'] || path.join(outDir, MANIFEST_FILE)
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
const promptPath = path.join(outDir, `upload-${safeSkillKey(hub)}.md`);
|
|
722
|
+
const promptResult = await writePrompt(absoluteRoot, { ...flags, hub, out: promptPath });
|
|
723
|
+
const inspectedAfterManifest = await inspectSkill(absoluteRoot, flags);
|
|
724
|
+
const disclosurePath = path.join(outDir, 'REGISTRY_DISCLOSURE.md');
|
|
725
|
+
await writeFile(
|
|
726
|
+
disclosurePath,
|
|
727
|
+
`${publicDisclosure(inspectedAfterManifest, inspectedAfterManifest.registry?.registryUrl || DEFAULT_REGISTRY_URL)
|
|
728
|
+
.filter((line) => line !== '公开披露文本:' && line !== '```markdown' && line !== '```')
|
|
729
|
+
.join('\n')}\n`,
|
|
730
|
+
{ mode: 0o600 }
|
|
731
|
+
);
|
|
732
|
+
const snippetPath = path.join(outDir, 'runtime-events.js');
|
|
733
|
+
let snippetResult = null;
|
|
734
|
+
if (!flags['no-snippet']) {
|
|
735
|
+
snippetResult = await writeRuntimeSnippet(absoluteRoot, {
|
|
736
|
+
...flags,
|
|
737
|
+
target: flags.target || 'js',
|
|
738
|
+
out: snippetPath
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const warnings = [];
|
|
743
|
+
let promptEvent = null;
|
|
744
|
+
if (!flags['no-track']) {
|
|
745
|
+
try {
|
|
746
|
+
promptEvent = await submitTrackEvent(absoluteRoot, {
|
|
747
|
+
...flags,
|
|
748
|
+
hub,
|
|
749
|
+
event: 'upload_prompt_generated',
|
|
750
|
+
'event-source': 'cli',
|
|
751
|
+
'runtime-kind': 'agent-handoff',
|
|
752
|
+
scenario: flags.scenario || 'third-party-skill-upload'
|
|
753
|
+
});
|
|
754
|
+
} catch (error) {
|
|
755
|
+
warnings.push(`upload_prompt_generated was not recorded: ${error instanceof Error ? error.message : String(error)}`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
return {
|
|
760
|
+
ok: true,
|
|
761
|
+
hub,
|
|
762
|
+
manifestPath: manifestResult.manifestPath,
|
|
763
|
+
promptPath: typeof promptResult === 'string' ? promptPath : promptResult.outPath,
|
|
764
|
+
disclosurePath,
|
|
765
|
+
snippetPath: snippetResult ? snippetResult.outPath : null,
|
|
766
|
+
promptEvent,
|
|
767
|
+
warnings,
|
|
768
|
+
next: [
|
|
769
|
+
`Open ${path.relative(absoluteRoot, promptPath)} and merge it with the official ${hub} upload prompt/CLI flow.`,
|
|
770
|
+
`If ${hub} provides its own upload prompt or CLI, treat that official flow as authoritative.`,
|
|
771
|
+
`Use ${path.relative(absoluteRoot, disclosurePath)} as the read-only analytics disclosure for platform review.`,
|
|
772
|
+
'Do not upload secrets, .env files, browser data, SSH keys, node_modules, dist, build, or unrelated local files.',
|
|
773
|
+
`If the Skill is published, record the public URL with: ${COMMAND_NAME} track . --event hub_upload_succeeded --hub ${hub} --platform-skill-url <url>`,
|
|
774
|
+
snippetResult
|
|
775
|
+
? `If the runtime supports HTTP/MCP/webhook, adapt ${path.relative(absoluteRoot, snippetPath)} into the Skill runtime.`
|
|
776
|
+
: 'Runtime snippet was skipped.'
|
|
777
|
+
]
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
|
|
429
781
|
async function main() {
|
|
430
782
|
const [command = '', ...commandArgs] = process.argv.slice(2);
|
|
431
783
|
if (!command || command === '--help' || command === '-h') {
|
|
@@ -447,14 +799,38 @@ async function main() {
|
|
|
447
799
|
print(result, flags.json);
|
|
448
800
|
return;
|
|
449
801
|
}
|
|
802
|
+
if (command === 'setup' || command === 'handoff') {
|
|
803
|
+
const result = await setupAgentWorkflow(root, flags);
|
|
804
|
+
if (flags.json) {
|
|
805
|
+
print(result, true);
|
|
806
|
+
} else {
|
|
807
|
+
const lines = [
|
|
808
|
+
`Wrote ${result.manifestPath}`,
|
|
809
|
+
`Wrote ${result.promptPath}`,
|
|
810
|
+
`Wrote ${result.disclosurePath}`,
|
|
811
|
+
result.snippetPath ? `Wrote ${result.snippetPath}` : null,
|
|
812
|
+
...result.warnings.map((warning) => `Warning: ${warning}`),
|
|
813
|
+
'',
|
|
814
|
+
'Next:',
|
|
815
|
+
...result.next.map((item) => `- ${item}`)
|
|
816
|
+
].filter(Boolean);
|
|
817
|
+
print(lines.join('\n'), false);
|
|
818
|
+
}
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
450
821
|
if (command === 'attach' || command === 'link') {
|
|
451
822
|
const result = await writeManifest(root, flags);
|
|
452
823
|
print(flags.json ? result : `Wrote ${result.manifestPath}`, flags.json);
|
|
453
824
|
return;
|
|
454
825
|
}
|
|
455
826
|
if (command === 'pack') {
|
|
456
|
-
const
|
|
457
|
-
|
|
827
|
+
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.`;
|
|
828
|
+
if (flags.json) {
|
|
829
|
+
print({ ok: false, error: 'pack_disabled', message }, true);
|
|
830
|
+
process.exitCode = 2;
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
fail(message, 2);
|
|
458
834
|
return;
|
|
459
835
|
}
|
|
460
836
|
if (command === 'prompt') {
|
|
@@ -466,6 +842,20 @@ async function main() {
|
|
|
466
842
|
}
|
|
467
843
|
return;
|
|
468
844
|
}
|
|
845
|
+
if (command === 'snippet') {
|
|
846
|
+
const result = await writeRuntimeSnippet(root, flags);
|
|
847
|
+
if (typeof result === 'string' || flags.json) {
|
|
848
|
+
print(result, flags.json);
|
|
849
|
+
} else {
|
|
850
|
+
print(`Wrote ${result.outPath}`, false);
|
|
851
|
+
}
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
if (command === 'track') {
|
|
855
|
+
const result = await submitTrackEvent(root, flags);
|
|
856
|
+
print(result, flags.json);
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
469
859
|
fail(`Unknown command: ${command}\n\n${usage()}`, 2);
|
|
470
860
|
} catch (error) {
|
|
471
861
|
fail(error instanceof Error ? error.message : String(error || 'xiashe-skill failed'), 1);
|