@xiashe/skill 0.1.4 → 0.1.7
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 +11 -3
- package/bin/xiashe-skill.mjs +415 -32
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,22 +9,30 @@ 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
|
+
- run one Agent-friendly registry setup command that prepares all supported hub handoffs
|
|
13
|
+
- diagnose whether a local Skill has the expected registry disclosure and runtime callback wiring
|
|
14
|
+
- send labeled local verification events so the Dashboard can confirm integration health
|
|
13
15
|
- write an explicit `xiashe.skill.json` registry manifest
|
|
14
|
-
- generate
|
|
16
|
+
- generate one unified `UPLOAD_HANDOFF.md` that the creator's Agent can use after the user pastes a third-party official upload prompt
|
|
15
17
|
- generate optional runtime analytics snippets
|
|
16
18
|
|
|
17
19
|
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.
|
|
18
20
|
|
|
21
|
+
`setup --hub all` still writes platform-specific `upload-<hub>.md` checklists for source attribution, but those are internal Agent checklists. Users should not need to pick files manually. When a platform such as Red Skill provides its own upload prompt, paste that official prompt to the Agent and tell it to follow `.xiashe/UPLOAD_HANDOFF.md`.
|
|
22
|
+
|
|
19
23
|
## Local development
|
|
20
24
|
|
|
21
25
|
```bash
|
|
22
26
|
node packages/xiashe-skill-cli/bin/xiashe-skill.mjs --help
|
|
23
27
|
node packages/xiashe-skill-cli/bin/xiashe-skill.mjs inspect .
|
|
28
|
+
node packages/xiashe-skill-cli/bin/xiashe-skill.mjs doctor .
|
|
29
|
+
node packages/xiashe-skill-cli/bin/xiashe-skill.mjs setup . --code XS-XXXX-XXXX --hub all
|
|
24
30
|
node packages/xiashe-skill-cli/bin/xiashe-skill.mjs setup . --code XS-XXXX-XXXX --hub red
|
|
31
|
+
node packages/xiashe-skill-cli/bin/xiashe-skill.mjs verify . --hub red --dry-run --json
|
|
32
|
+
node packages/xiashe-skill-cli/bin/xiashe-skill.mjs verify . --hub red
|
|
25
33
|
node packages/xiashe-skill-cli/bin/xiashe-skill.mjs attach . --code XS-XXXX-XXXX
|
|
26
34
|
node packages/xiashe-skill-cli/bin/xiashe-skill.mjs attach . --public-token pub_xxx --skill-id sk_xxx
|
|
27
35
|
node packages/xiashe-skill-cli/bin/xiashe-skill.mjs prompt . --hub red --source-url https://example.com/my-skill-repo
|
|
28
36
|
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
|
|
37
|
+
node packages/xiashe-skill-cli/bin/xiashe-skill.mjs track . --event skill_invoked --hub coze --dry-run --json
|
|
30
38
|
```
|
package/bin/xiashe-skill.mjs
CHANGED
|
@@ -6,7 +6,7 @@ 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.7';
|
|
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');
|
|
@@ -17,11 +17,14 @@ const MANIFEST_FILE = process.env.XIASHE_SKILL_MANIFEST_FILE || (COMMAND_NAME ==
|
|
|
17
17
|
? 'agentpie.skill.json'
|
|
18
18
|
: 'xiashe.skill.json');
|
|
19
19
|
const WORK_DIR = process.env.XIASHE_SKILL_WORK_DIR || (COMMAND_NAME === 'agentpie-skill' ? '.agentpie' : '.xiashe');
|
|
20
|
+
const HANDOFF_FILE = 'UPLOAD_HANDOFF.md';
|
|
20
21
|
const DEFAULT_REGISTRY_URL = process.env.XIASHE_SKILL_REGISTRY_URL || `${DEFAULT_BASE_URL}/registry/skill-events`;
|
|
21
22
|
const DEFAULT_CLAIM_URL = process.env.XIASHE_SKILL_CLAIM_URL || `${DEFAULT_BASE_URL}/registry/skill/claim`;
|
|
22
23
|
const EVENT_SCHEMA_VERSION = process.env.XIASHE_SKILL_EVENT_SCHEMA_VERSION || (REGISTRY_PROVIDER === 'agentpie'
|
|
23
24
|
? 'agentpie.skill.event.v1'
|
|
24
25
|
: 'xiashe.skill.event.v1');
|
|
26
|
+
const SUPPORTED_HUBS = ['red', 'clawhub', 'skillhub', 'claude', 'dify', 'coze', 'generic'];
|
|
27
|
+
const DEFAULT_SETUP_HUBS = ['red', 'clawhub', 'skillhub', 'claude', 'dify', 'coze', 'generic'];
|
|
25
28
|
const DEFAULT_IGNORE = new Set([
|
|
26
29
|
'.git',
|
|
27
30
|
'.xiashe',
|
|
@@ -44,11 +47,13 @@ function usage() {
|
|
|
44
47
|
|
|
45
48
|
Usage:
|
|
46
49
|
${COMMAND_NAME} inspect [skill-dir] [--json]
|
|
47
|
-
${COMMAND_NAME} setup [skill-dir] --code <dynamic-code> --hub
|
|
48
|
-
${COMMAND_NAME} setup [skill-dir] --public-token <token> --skill-id <id> --hub
|
|
50
|
+
${COMMAND_NAME} setup [skill-dir] --code <dynamic-code> [--hub all|red|clawhub|skillhub|claude|dify|coze|generic]
|
|
51
|
+
${COMMAND_NAME} setup [skill-dir] --public-token <token> --skill-id <id> [--hub all|red|clawhub|skillhub|claude|dify|coze|generic]
|
|
52
|
+
${COMMAND_NAME} doctor [skill-dir] [--json]
|
|
53
|
+
${COMMAND_NAME} verify [skill-dir] [--hub <hub>] [--scenario <label>] [--dry-run]
|
|
49
54
|
${COMMAND_NAME} attach [skill-dir] --code <dynamic-code> [--name <name>]
|
|
50
55
|
${COMMAND_NAME} attach [skill-dir] --public-token <token> [--skill-id <id>] [--name <name>]
|
|
51
|
-
${COMMAND_NAME} prompt [skill-dir] --hub <red|clawhub|skillhub|claude|dify|generic> [--source-url <url>] [--out <file>]
|
|
56
|
+
${COMMAND_NAME} prompt [skill-dir] --hub <red|clawhub|skillhub|claude|dify|coze|generic> [--source-url <url>] [--out <file>]
|
|
52
57
|
${COMMAND_NAME} snippet [skill-dir] [--target js|curl] [--out <file>]
|
|
53
58
|
${COMMAND_NAME} track [skill-dir] --event <event> [--hub <hub>] [--installation-id <id>] [--invocation-id <id>]
|
|
54
59
|
|
|
@@ -60,7 +65,7 @@ Options:
|
|
|
60
65
|
--registry-url <url> Event endpoint written to the manifest.
|
|
61
66
|
--name <name> Skill display name override.
|
|
62
67
|
--description <text> Skill description override.
|
|
63
|
-
--hub <hub> Target Skill hub prompt style
|
|
68
|
+
--hub <hub> Target Skill hub prompt style. setup defaults to all supported hubs.
|
|
64
69
|
--event <event> Runtime event to submit for testing.
|
|
65
70
|
--installation-id <id> Stable anonymous install id for runtime analytics.
|
|
66
71
|
--invocation-id <id> Unique invocation id for one skill call.
|
|
@@ -150,6 +155,37 @@ function safeSkillKey(value) {
|
|
|
150
155
|
.replace(/^-+|-+$/g, '') || 'skill';
|
|
151
156
|
}
|
|
152
157
|
|
|
158
|
+
function normalizeHub(value) {
|
|
159
|
+
const hub = normalizeText(value, 40).toLowerCase().replace(/\s+/g, '').replace(/_/g, '-');
|
|
160
|
+
if (hub === 'redskill' || hub === 'red-skill' || hub === 'xiaohongshu' || hub === 'xhs') {
|
|
161
|
+
return 'red';
|
|
162
|
+
}
|
|
163
|
+
if (hub === 'claw-hub') {
|
|
164
|
+
return 'clawhub';
|
|
165
|
+
}
|
|
166
|
+
if (hub === 'skill-hub') {
|
|
167
|
+
return 'skillhub';
|
|
168
|
+
}
|
|
169
|
+
return hub;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function setupHubsFromFlags(flags) {
|
|
173
|
+
const raw = normalizeText(flags.hub || 'all', 240).toLowerCase();
|
|
174
|
+
if (!raw || raw === 'all' || raw === '*') {
|
|
175
|
+
return [...DEFAULT_SETUP_HUBS];
|
|
176
|
+
}
|
|
177
|
+
const hubs = raw
|
|
178
|
+
.split(',')
|
|
179
|
+
.map((item) => normalizeHub(item))
|
|
180
|
+
.filter(Boolean);
|
|
181
|
+
const unique = Array.from(new Set(hubs));
|
|
182
|
+
const unsupported = unique.filter((hub) => !SUPPORTED_HUBS.includes(hub));
|
|
183
|
+
if (unsupported.length > 0) {
|
|
184
|
+
throw new Error(`Unsupported --hub value: ${unsupported.join(', ')}. Use all, ${SUPPORTED_HUBS.join(', ')}.`);
|
|
185
|
+
}
|
|
186
|
+
return unique.length > 0 ? unique : [...DEFAULT_SETUP_HUBS];
|
|
187
|
+
}
|
|
188
|
+
|
|
153
189
|
function canonicalSignaturePayload(payload) {
|
|
154
190
|
return [
|
|
155
191
|
payload.schemaVersion || EVENT_SCHEMA_VERSION,
|
|
@@ -340,6 +376,7 @@ async function writeManifest(root, flags) {
|
|
|
340
376
|
},
|
|
341
377
|
analytics: {
|
|
342
378
|
enabled: true,
|
|
379
|
+
supportedHubs: [...SUPPORTED_HUBS],
|
|
343
380
|
allowedEvents: [
|
|
344
381
|
'upload_prompt_generated',
|
|
345
382
|
'hub_upload_attempted',
|
|
@@ -353,6 +390,20 @@ async function writeManifest(root, flags) {
|
|
|
353
390
|
]
|
|
354
391
|
}
|
|
355
392
|
},
|
|
393
|
+
platforms: Object.fromEntries(SUPPORTED_HUBS.map((hub) => [
|
|
394
|
+
hub,
|
|
395
|
+
{
|
|
396
|
+
hub,
|
|
397
|
+
sourceSurface: hub,
|
|
398
|
+
uploadPromptFile: `${WORK_DIR}/upload-${safeSkillKey(hub)}.md`,
|
|
399
|
+
eventDefaults: {
|
|
400
|
+
schemaVersion: EVENT_SCHEMA_VERSION,
|
|
401
|
+
hub,
|
|
402
|
+
sourceSurface: hub,
|
|
403
|
+
skillKey: safeSkillKey(claim?.skillKey || inspected.skillKey)
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
])),
|
|
356
407
|
sourceFingerprint: {
|
|
357
408
|
sha256: inspected.package.sha256,
|
|
358
409
|
fileCount: inspected.package.fileCount,
|
|
@@ -386,6 +437,9 @@ function hubInstruction(hub) {
|
|
|
386
437
|
if (hub === 'dify') {
|
|
387
438
|
return 'Dify Plugin/Tool 的官方打包和发布流程是主流程。请先按 Dify 插件规范处理 manifest 和工具定义,再把下面的 registry 披露文本放入允许的 README/说明字段。';
|
|
388
439
|
}
|
|
440
|
+
if (hub === 'coze') {
|
|
441
|
+
return 'Coze Agent/Plugin/Workflow 的官方配置和发布流程是主流程。请先按 Coze 当前工具、插件或知识库要求配置,再把下面的 registry 回调作为可选 HTTP/API 步骤接入。';
|
|
442
|
+
}
|
|
389
443
|
return '目标 Skill Hub 的官方 CLI、prompt 或页面流程是主流程。请先确认平台当前要求,再把下面的虾舍 registry 要求作为附加说明合并进去。';
|
|
390
444
|
}
|
|
391
445
|
|
|
@@ -405,6 +459,9 @@ function platformPromptPlaceholder(hub) {
|
|
|
405
459
|
if (hub === 'dify') {
|
|
406
460
|
return '如果 Dify 提供官方插件打包/发布命令,请优先使用官方命令;不要用虾舍 prompt 替代 Dify 官方流程。';
|
|
407
461
|
}
|
|
462
|
+
if (hub === 'coze') {
|
|
463
|
+
return '如果 Coze 提供 Agent/Plugin/Workflow 的配置步骤,请优先使用 Coze 官方配置;不要用虾舍 prompt 替代 Coze 官方流程。';
|
|
464
|
+
}
|
|
408
465
|
return '如果目标平台提供官方 CLI 命令或上传 prompt,请优先使用官方命令/prompt;不要用虾舍 prompt 替代平台官方流程。';
|
|
409
466
|
}
|
|
410
467
|
|
|
@@ -508,7 +565,7 @@ async function readPlatformPrompt(flags) {
|
|
|
508
565
|
}
|
|
509
566
|
|
|
510
567
|
async function uploadPrompt(inspected, flags) {
|
|
511
|
-
const hub =
|
|
568
|
+
const hub = normalizeHub(flags.hub || 'generic') || 'generic';
|
|
512
569
|
const sourceUrl = normalizeText(flags['source-url'] || flags['package-url'], 1000);
|
|
513
570
|
const platformCommand = normalizeText(flags['platform-command'], 1000);
|
|
514
571
|
const platformPrompt = await readPlatformPrompt(flags);
|
|
@@ -582,8 +639,10 @@ async function uploadPrompt(inspected, flags) {
|
|
|
582
639
|
'- 不要新增 postinstall hook、后台常驻进程、隐藏脚本或混淆逻辑。',
|
|
583
640
|
`- 如果平台要求安全说明,请明确说明这是用户显式发布的 Skill 项目,${MANIFEST_FILE} 只包含公开 registry 归因和可关闭的 analytics 声明。`,
|
|
584
641
|
`- 如果目标平台支持工具调用、MCP server、webhook 或 Skill runtime HTTP 请求,请在 Skill 被调用时向 ${PRODUCT_NAME} registry 发送聚合事件:skill_invoked / skill_completed / skill_failed。`,
|
|
642
|
+
`- 如果 runtime 没有向 ${registryUrl} 发送事件,${PRODUCT_NAME} 无法看到真实调用量;第三方平台内部显示“已使用”不会自动同步到 ${PRODUCT_NAME}。`,
|
|
585
643
|
'- 事件里只放公开元数据、聚合状态和简短场景标签,不要发送用户输入原文、私密业务内容、账号凭据或文件内容。',
|
|
586
644
|
'- installationId 必须是稳定的匿名实例 ID,用于统计唯一安装实例;不要使用邮箱、手机号、真实用户名或设备序列号。',
|
|
645
|
+
'- 发布前请至少发送一次测试事件,并确认 Dashboard 能看到对应 hub/sourceSurface;否则把这个平台标记为“仅上传/手动上报”。',
|
|
587
646
|
'',
|
|
588
647
|
`发布成功后,请把公开链接和平台返回的信息回传给${PRODUCT_NAME} registry。只发送公开元数据和聚合事件,不发送用户内容、私密输入或账号凭据。`,
|
|
589
648
|
'',
|
|
@@ -605,7 +664,7 @@ async function uploadPrompt(inspected, flags) {
|
|
|
605
664
|
|
|
606
665
|
async function writePrompt(root, flags) {
|
|
607
666
|
if (!flags.hub) {
|
|
608
|
-
throw new Error(
|
|
667
|
+
throw new Error(`Missing --hub <${SUPPORTED_HUBS.join('|')}>.`);
|
|
609
668
|
}
|
|
610
669
|
const inspected = await inspectSkill(root, flags);
|
|
611
670
|
const prompt = await uploadPrompt(inspected, flags);
|
|
@@ -618,6 +677,94 @@ async function writePrompt(root, flags) {
|
|
|
618
677
|
return prompt;
|
|
619
678
|
}
|
|
620
679
|
|
|
680
|
+
async function writeUnifiedHandoff(root, args) {
|
|
681
|
+
const relativeOutDir = path.relative(root, args.outDir) || WORK_DIR;
|
|
682
|
+
const promptIndex = args.promptResults
|
|
683
|
+
.map((item) => `- ${item.hub}: ${path.relative(root, item.promptPath)}`)
|
|
684
|
+
.join('\n');
|
|
685
|
+
const registry = args.inspected.registry || {};
|
|
686
|
+
const registryUrl = registry.registryUrl || DEFAULT_REGISTRY_URL;
|
|
687
|
+
const content = [
|
|
688
|
+
`# ${PRODUCT_NAME} Skill Upload Handoff`,
|
|
689
|
+
'',
|
|
690
|
+
`Skill: ${args.inspected.name}`,
|
|
691
|
+
`Skill Key: ${args.inspected.skillKey}`,
|
|
692
|
+
`Supported hubs prepared: ${args.hubs.join(', ')}`,
|
|
693
|
+
'',
|
|
694
|
+
'## How to use this file',
|
|
695
|
+
'',
|
|
696
|
+
'This is the single handoff file for the user\'s Agent. Do not ask the user to manually choose or inspect registry files.',
|
|
697
|
+
'',
|
|
698
|
+
'When the user later pastes an official third-party platform upload prompt, CLI command, or page instructions:',
|
|
699
|
+
'',
|
|
700
|
+
'1. Identify the target platform from that official prompt, URL, command, or user message.',
|
|
701
|
+
`2. Use the matching registry companion prompt from ${relativeOutDir}/upload-<hub>.md only as an internal checklist.`,
|
|
702
|
+
'3. Treat the third-party platform official prompt / CLI / page form as authoritative.',
|
|
703
|
+
`4. Merge only the safe ${PRODUCT_NAME} registry additions: public disclosure, source attribution, hub value, and optional runtime event callback.`,
|
|
704
|
+
'5. If the platform rejects extra files, do not upload registry JSON/snippets. Put the disclosure text into SKILL.md, README, or the platform description field only when allowed.',
|
|
705
|
+
'6. If the Skill exposes an MCP tool, API endpoint, webhook, script entrypoint, or Agent-executable function, add best-effort runtime event callbacks at the real invocation boundary.',
|
|
706
|
+
'7. Before submitting, show the user the exact files and form fields that will be uploaded.',
|
|
707
|
+
'8. Never upload .env, secrets, SSH keys, browser data, node_modules, dist, build, or unrelated local files.',
|
|
708
|
+
'',
|
|
709
|
+
'## Platform mapping',
|
|
710
|
+
'',
|
|
711
|
+
promptIndex,
|
|
712
|
+
'',
|
|
713
|
+
'## Official prompt merge rule',
|
|
714
|
+
'',
|
|
715
|
+
'If the user gives a Red Skill / ClawHub / SkillHub / Claude / Dify / Coze official prompt, do not replace it. Append these registry requirements as constraints, and follow the official prompt for packaging, file format, and upload steps.',
|
|
716
|
+
'',
|
|
717
|
+
'If the target platform is unclear, ask one short question: "Which Skill platform should I upload to?"',
|
|
718
|
+
'',
|
|
719
|
+
'## Analytics callback',
|
|
720
|
+
'',
|
|
721
|
+
`Registry endpoint: ${registryUrl}`,
|
|
722
|
+
`Event schema: ${registry.eventSchemaVersion || EVENT_SCHEMA_VERSION}`,
|
|
723
|
+
'',
|
|
724
|
+
'Only add runtime event callbacks when the target platform or runtime explicitly supports HTTP/API/MCP/webhook calls. Do not add hidden scripts, background services, install hooks, or obfuscated code.',
|
|
725
|
+
'',
|
|
726
|
+
`No runtime callback means no live ${PRODUCT_NAME} analytics. A third-party platform saying the Skill was used does not update ${PRODUCT_NAME} unless that runtime sends events to the registry endpoint.`,
|
|
727
|
+
'',
|
|
728
|
+
'Best-effort callback placement:',
|
|
729
|
+
'',
|
|
730
|
+
'- Send `runtime_install_seen` once when a stable anonymous installation/session is first observed.',
|
|
731
|
+
'- Send `skill_invoked` at the start of the actual Skill execution, not when the upload prompt is generated.',
|
|
732
|
+
'- Send `skill_completed` after successful completion and `skill_failed` after handled failure.',
|
|
733
|
+
'- Use one stable anonymous `installationId` per installed copy/runtime connection. Never use email, phone, real name, device serial, raw prompt text, or private content.',
|
|
734
|
+
'- Use one `invocationId` per execution and use it in the idempotency key so retries do not double count.',
|
|
735
|
+
'',
|
|
736
|
+
'Required source separation:',
|
|
737
|
+
'',
|
|
738
|
+
'```json',
|
|
739
|
+
JSON.stringify({
|
|
740
|
+
hub: '<red|clawhub|skillhub|claude|dify|coze|generic>',
|
|
741
|
+
sourceSurface: '<same as hub unless the platform requires another public label>',
|
|
742
|
+
scenario: '<short public label only>',
|
|
743
|
+
installationId: '<stable anonymous install id>',
|
|
744
|
+
invocationId: '<unique invocation id>'
|
|
745
|
+
}, null, 2),
|
|
746
|
+
'```',
|
|
747
|
+
'',
|
|
748
|
+
'After a successful publication, record the public URL with:',
|
|
749
|
+
'',
|
|
750
|
+
'```bash',
|
|
751
|
+
`${COMMAND_NAME} track . --event hub_upload_succeeded --hub <hub> --platform-skill-url <published-url>`,
|
|
752
|
+
'```',
|
|
753
|
+
'',
|
|
754
|
+
'Before submitting to a hub that supports runtime HTTP/API calls, verify analytics with:',
|
|
755
|
+
'',
|
|
756
|
+
'```bash',
|
|
757
|
+
`${COMMAND_NAME} track . --event skill_invoked --hub <hub> --installation-id <anonymous-install-id> --invocation-id <test-call-id> --scenario test`,
|
|
758
|
+
'```',
|
|
759
|
+
'',
|
|
760
|
+
'If the hub cannot run callbacks, keep upload attribution and use campaign links or manual imports. Do not fake runtime events.',
|
|
761
|
+
''
|
|
762
|
+
].join('\n');
|
|
763
|
+
const handoffPath = path.join(args.outDir, HANDOFF_FILE);
|
|
764
|
+
await writeFile(handoffPath, content, { mode: 0o600 });
|
|
765
|
+
return handoffPath;
|
|
766
|
+
}
|
|
767
|
+
|
|
621
768
|
async function writeRuntimeSnippet(root, flags) {
|
|
622
769
|
const inspected = await inspectSkill(root, flags);
|
|
623
770
|
const registry = inspected.registry || {};
|
|
@@ -727,7 +874,7 @@ async function submitTrackEvent(root, flags) {
|
|
|
727
874
|
const occurredAt = Number(flags['occurred-at'] || Date.now());
|
|
728
875
|
const invocationId = normalizeText(flags['invocation-id'] || `cli-${Date.now()}`, 160);
|
|
729
876
|
const installationId = normalizeText(flags['installation-id'] || 'local-test-install', 220);
|
|
730
|
-
const hub =
|
|
877
|
+
const hub = normalizeHub(flags.hub || 'local-test') || 'local-test';
|
|
731
878
|
const payload = {
|
|
732
879
|
publicToken,
|
|
733
880
|
schemaVersion: normalizeText(flags['schema-version'], 80) || EVENT_SCHEMA_VERSION,
|
|
@@ -758,8 +905,211 @@ async function submitTrackEvent(root, flags) {
|
|
|
758
905
|
return { ok: true, registryUrl, payload, result };
|
|
759
906
|
}
|
|
760
907
|
|
|
908
|
+
async function readSmallTextFile(filePath) {
|
|
909
|
+
const info = await stat(filePath).catch(() => null);
|
|
910
|
+
if (!info?.isFile() || info.size > MAX_FILE_BYTES) return '';
|
|
911
|
+
try {
|
|
912
|
+
return await readFile(filePath, 'utf8');
|
|
913
|
+
} catch {
|
|
914
|
+
return '';
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
async function findReadmePath(root) {
|
|
919
|
+
const entries = await readdir(root, { withFileTypes: true }).catch(() => []);
|
|
920
|
+
const readme = entries
|
|
921
|
+
.filter((entry) => entry.isFile() && /^readme(\.|$)/i.test(entry.name))
|
|
922
|
+
.map((entry) => path.join(root, entry.name))
|
|
923
|
+
.sort((a, b) => a.localeCompare(b))[0];
|
|
924
|
+
return readme || path.join(root, 'README.md');
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function registryBlockPresent(text) {
|
|
928
|
+
return /<!--\s*(?:xiashe|agentpie)-registry:start\s*-->[\s\S]*?<!--\s*(?:xiashe|agentpie)-registry:end\s*-->/i.test(text || '');
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function hasEntryInstructions(text, packageJson) {
|
|
932
|
+
if (packageJson?.main || packageJson?.bin || packageJson?.scripts) return true;
|
|
933
|
+
return /(入口|使用|安装|运行|调用|命令|Usage|Install|Quick start|Getting started|CLI|MCP|API|Entrypoint|Run)/i.test(text || '');
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function hasRuntimeCallbackText(text) {
|
|
937
|
+
return /(\/registry\/skill-events|trackSkillEvent|runtime_install_seen|skill_invoked|skill_completed|skill_failed|XIASHE_SKILL_REGISTRY_URL|AGENTPIE_SKILL_REGISTRY_URL)/i.test(text || '');
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function doctorCheck(id, label, status, message, fix = '') {
|
|
941
|
+
return { id, label, status, message, fix };
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
async function runDoctor(root, flags) {
|
|
945
|
+
const inspected = await inspectSkill(root, flags);
|
|
946
|
+
const skillMdPath = path.join(inspected.root, 'SKILL.md');
|
|
947
|
+
const readmePath = await findReadmePath(inspected.root);
|
|
948
|
+
const manifestPath = path.join(inspected.root, MANIFEST_FILE);
|
|
949
|
+
const localManifestPath = path.join(inspected.root, WORK_DIR, MANIFEST_FILE);
|
|
950
|
+
const disclosurePath = path.join(inspected.root, WORK_DIR, 'REGISTRY_DISCLOSURE.md');
|
|
951
|
+
const handoffPath = path.join(inspected.root, WORK_DIR, HANDOFF_FILE);
|
|
952
|
+
const snippetPath = path.join(inspected.root, WORK_DIR, 'runtime-events.js');
|
|
953
|
+
const packageJson = await readJsonFile(path.join(inspected.root, 'package.json')).catch(() => null);
|
|
954
|
+
const skillMd = await readSmallTextFile(skillMdPath);
|
|
955
|
+
const readme = await readSmallTextFile(readmePath);
|
|
956
|
+
const snippet = await readSmallTextFile(snippetPath);
|
|
957
|
+
const scannedCallbackFiles = [];
|
|
958
|
+
let scannedCallbackText = '';
|
|
959
|
+
for (const file of inspected.package.files.slice(0, 160)) {
|
|
960
|
+
if (!/\.(md|mdx|txt|json|ya?ml|toml|js|mjs|cjs|ts|tsx|jsx|py|go|rs|java|rb|php|sh)$/i.test(file.path)) continue;
|
|
961
|
+
const absolute = path.join(inspected.root, file.path);
|
|
962
|
+
const text = await readSmallTextFile(absolute);
|
|
963
|
+
if (hasRuntimeCallbackText(text)) {
|
|
964
|
+
scannedCallbackFiles.push(file.path);
|
|
965
|
+
scannedCallbackText += `\n${text}`;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
const manifest = inspected.registry
|
|
969
|
+
? await readJsonFile(manifestPath).catch(() => null) || await readJsonFile(localManifestPath).catch(() => null)
|
|
970
|
+
: null;
|
|
971
|
+
const textBundle = `${skillMd}\n${readme}`;
|
|
972
|
+
const hasManifest = Boolean(manifest);
|
|
973
|
+
const registry = manifest?.registry || inspected.registry || {};
|
|
974
|
+
const runtimeCallbackPresent = hasRuntimeCallbackText(snippet) || hasRuntimeCallbackText(textBundle) || hasRuntimeCallbackText(scannedCallbackText);
|
|
975
|
+
const riskyRootEntries = ['.env', '.env.local', '.env.production', 'node_modules', 'dist', 'build']
|
|
976
|
+
.filter((name) => existsSync(path.join(inspected.root, name)));
|
|
977
|
+
const checks = [
|
|
978
|
+
existsSync(skillMdPath)
|
|
979
|
+
? doctorCheck('skill_md', 'SKILL.md', 'pass', 'Found SKILL.md.')
|
|
980
|
+
: doctorCheck('skill_md', 'SKILL.md', 'fail', 'SKILL.md is missing.', 'Create SKILL.md with the Skill purpose, usage, and safety notes.'),
|
|
981
|
+
existsSync(readmePath)
|
|
982
|
+
? doctorCheck('readme', 'README', 'pass', `Found ${path.basename(readmePath)}.`)
|
|
983
|
+
: doctorCheck('readme', 'README', 'warn', 'README is missing.', 'Add README.md if the target hub expects public documentation.'),
|
|
984
|
+
hasEntryInstructions(textBundle, packageJson)
|
|
985
|
+
? doctorCheck('entry_instructions', 'Entry instructions', 'pass', 'Usage or entry instructions are present.')
|
|
986
|
+
: doctorCheck('entry_instructions', 'Entry instructions', 'warn', 'No clear usage/entry instructions detected.', 'Add usage, install, MCP/API, or command instructions to SKILL.md or README.'),
|
|
987
|
+
hasManifest && registry.publicToken && registry.registryUrl
|
|
988
|
+
? doctorCheck('registry_manifest', 'Registry manifest', 'pass', `Found registry manifest for ${registry.provider || REGISTRY_PROVIDER}.`)
|
|
989
|
+
: doctorCheck('registry_manifest', 'Registry manifest', 'fail', `Missing usable registry manifest (${MANIFEST_FILE} or ${WORK_DIR}/${MANIFEST_FILE}).`, `Run ${COMMAND_NAME} setup . --code <dashboard-code> or ${COMMAND_NAME} attach . --public-token <token> --skill-id <id>.`),
|
|
990
|
+
registryBlockPresent(skillMd)
|
|
991
|
+
? doctorCheck('registry_block', 'Registry block', 'pass', 'SKILL.md contains the explicit registry disclosure block.')
|
|
992
|
+
: doctorCheck('registry_block', 'Registry block', 'warn', 'SKILL.md does not contain a registry block.', `Run ${COMMAND_NAME} setup . --code <code> --embed-skill-md, especially for restrictive hubs such as Red Skill.`),
|
|
993
|
+
runtimeCallbackPresent
|
|
994
|
+
? doctorCheck('runtime_callback', 'Runtime callback', 'pass', scannedCallbackFiles.length > 0 ? `Runtime callback references found in ${scannedCallbackFiles.slice(0, 5).join(', ')}.` : 'Runtime callback snippet/reference detected.')
|
|
995
|
+
: doctorCheck('runtime_callback', 'Runtime callback', 'warn', 'No runtime callback reference detected.', `Use ${COMMAND_NAME} snippet . --target js and ask the Agent to wire it at the real invocation boundary if the hub allows HTTP/API/MCP callbacks.`),
|
|
996
|
+
existsSync(disclosurePath)
|
|
997
|
+
? doctorCheck('disclosure', 'Read-only disclosure', 'pass', `Found ${path.relative(inspected.root, disclosurePath)}.`)
|
|
998
|
+
: doctorCheck('disclosure', 'Read-only disclosure', 'warn', 'REGISTRY_DISCLOSURE.md is missing.', `Run ${COMMAND_NAME} setup . --code <code> to generate platform review disclosure.`),
|
|
999
|
+
existsSync(handoffPath)
|
|
1000
|
+
? doctorCheck('handoff', 'Agent handoff', 'pass', `Found ${path.relative(inspected.root, handoffPath)}.`)
|
|
1001
|
+
: doctorCheck('handoff', 'Agent handoff', 'warn', 'Unified upload handoff is missing.', `Run ${COMMAND_NAME} setup . --code <code> --hub all.`),
|
|
1002
|
+
riskyRootEntries.length === 0
|
|
1003
|
+
? doctorCheck('risky_files', 'Risky local files', 'pass', 'No common risky root files/directories detected.')
|
|
1004
|
+
: doctorCheck('risky_files', 'Risky local files', 'warn', `Found ${riskyRootEntries.join(', ')} in the Skill root.`, 'Do not upload secrets, node_modules, dist/build output, or unrelated local files unless the target hub explicitly requires them.')
|
|
1005
|
+
];
|
|
1006
|
+
const failed = checks.filter((check) => check.status === 'fail').length;
|
|
1007
|
+
const warned = checks.filter((check) => check.status === 'warn').length;
|
|
1008
|
+
const passed = checks.filter((check) => check.status === 'pass').length;
|
|
1009
|
+
const score = Math.max(0, Math.round((passed / checks.length) * 100) - failed * 15 - warned * 4);
|
|
1010
|
+
return {
|
|
1011
|
+
ok: failed === 0,
|
|
1012
|
+
score,
|
|
1013
|
+
root: inspected.root,
|
|
1014
|
+
skillKey: inspected.skillKey,
|
|
1015
|
+
name: inspected.name,
|
|
1016
|
+
package: inspected.package,
|
|
1017
|
+
checks,
|
|
1018
|
+
next: checks
|
|
1019
|
+
.filter((check) => check.status !== 'pass' && check.fix)
|
|
1020
|
+
.map((check) => check.fix)
|
|
1021
|
+
.slice(0, 6)
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function formatDoctor(result) {
|
|
1026
|
+
const statusIcon = { pass: 'PASS', warn: 'WARN', fail: 'FAIL' };
|
|
1027
|
+
const lines = [
|
|
1028
|
+
`${COMMAND_NAME} doctor`,
|
|
1029
|
+
`Skill: ${result.name} (${result.skillKey})`,
|
|
1030
|
+
`Score: ${result.score}/100`,
|
|
1031
|
+
''
|
|
1032
|
+
];
|
|
1033
|
+
for (const check of result.checks) {
|
|
1034
|
+
lines.push(`${statusIcon[check.status] || check.status} ${check.label}: ${check.message}`);
|
|
1035
|
+
if (check.status !== 'pass' && check.fix) lines.push(` Fix: ${check.fix}`);
|
|
1036
|
+
}
|
|
1037
|
+
if (result.next.length > 0) {
|
|
1038
|
+
lines.push('', 'Next:');
|
|
1039
|
+
for (const item of result.next) lines.push(`- ${item}`);
|
|
1040
|
+
}
|
|
1041
|
+
return lines.join('\n');
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
async function verifyRegistry(root, flags) {
|
|
1045
|
+
const inspected = await inspectSkill(root, flags);
|
|
1046
|
+
const hub = normalizeHub(flags.hub || 'generic') || 'generic';
|
|
1047
|
+
const scenario = normalizeText(flags.scenario || `${REGISTRY_PROVIDER}-skill-verify`, 120);
|
|
1048
|
+
const timestamp = Date.now();
|
|
1049
|
+
const installationId = normalizeText(flags['installation-id'] || `verify-${REGISTRY_PROVIDER}-${timestamp}`, 220);
|
|
1050
|
+
const invocationId = normalizeText(flags['invocation-id'] || `verify-call-${timestamp}`, 160);
|
|
1051
|
+
const events = normalizeText(flags.events || '', 400)
|
|
1052
|
+
? normalizeText(flags.events, 400).split(',').map((item) => normalizeText(item, 80)).filter(Boolean)
|
|
1053
|
+
: ['runtime_install_seen', 'skill_invoked', 'skill_completed'];
|
|
1054
|
+
const results = [];
|
|
1055
|
+
for (const eventType of events) {
|
|
1056
|
+
const eventResult = await submitTrackEvent(root, {
|
|
1057
|
+
...flags,
|
|
1058
|
+
event: eventType,
|
|
1059
|
+
hub,
|
|
1060
|
+
'event-source': 'runtime',
|
|
1061
|
+
'runtime-kind': 'cli-verify',
|
|
1062
|
+
'source-surface': hub,
|
|
1063
|
+
scenario,
|
|
1064
|
+
'installation-id': installationId,
|
|
1065
|
+
'invocation-id': invocationId,
|
|
1066
|
+
'idempotency-key': `verify:${inspected.skillKey}:${hub}:${invocationId}:${eventType}`
|
|
1067
|
+
});
|
|
1068
|
+
results.push({ eventType, ...eventResult });
|
|
1069
|
+
}
|
|
1070
|
+
return {
|
|
1071
|
+
ok: results.every((result) => result.ok),
|
|
1072
|
+
dryRun: Boolean(flags.dryRun),
|
|
1073
|
+
skillKey: inspected.skillKey,
|
|
1074
|
+
hub,
|
|
1075
|
+
scenario,
|
|
1076
|
+
installationId,
|
|
1077
|
+
invocationId,
|
|
1078
|
+
registryUrl: results[0]?.registryUrl || inspected.registry?.registryUrl || DEFAULT_REGISTRY_URL,
|
|
1079
|
+
events: results,
|
|
1080
|
+
next: [
|
|
1081
|
+
'Open the creator dashboard and check 接入健康度 / Integration health.',
|
|
1082
|
+
'Verified should become active after these cli-verify events are indexed.',
|
|
1083
|
+
'If runtime remains inactive later, the third-party platform is not calling /registry/skill-events from real Skill execution.'
|
|
1084
|
+
]
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function formatVerify(result) {
|
|
1089
|
+
const lines = [
|
|
1090
|
+
`${COMMAND_NAME} verify`,
|
|
1091
|
+
`Skill key: ${result.skillKey}`,
|
|
1092
|
+
`Hub: ${result.hub}`,
|
|
1093
|
+
`Scenario: ${result.scenario}`,
|
|
1094
|
+
`Installation: ${result.installationId}`,
|
|
1095
|
+
`Invocation: ${result.invocationId}`,
|
|
1096
|
+
`Endpoint: ${result.registryUrl}`,
|
|
1097
|
+
''
|
|
1098
|
+
];
|
|
1099
|
+
for (const item of result.events) {
|
|
1100
|
+
if (item.dryRun) {
|
|
1101
|
+
lines.push(`DRY ${item.eventType}: ${JSON.stringify(item.payload)}`);
|
|
1102
|
+
} else {
|
|
1103
|
+
lines.push(`OK ${item.eventType}: ${item.result?.eventId || item.result?.status || 'accepted'}`);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
lines.push('', 'Next:');
|
|
1107
|
+
for (const item of result.next) lines.push(`- ${item}`);
|
|
1108
|
+
return lines.join('\n');
|
|
1109
|
+
}
|
|
1110
|
+
|
|
761
1111
|
async function setupAgentWorkflow(root, flags) {
|
|
762
|
-
const
|
|
1112
|
+
const hubs = setupHubsFromFlags(flags);
|
|
763
1113
|
const absoluteRoot = path.resolve(root || '.');
|
|
764
1114
|
const outDir = path.resolve(flags['out-dir'] || path.join(absoluteRoot, WORK_DIR));
|
|
765
1115
|
await mkdir(outDir, { recursive: true });
|
|
@@ -769,12 +1119,19 @@ async function setupAgentWorkflow(root, flags) {
|
|
|
769
1119
|
'manifest-path': flags['manifest-path'] || path.join(outDir, MANIFEST_FILE)
|
|
770
1120
|
});
|
|
771
1121
|
|
|
772
|
-
const
|
|
773
|
-
|
|
1122
|
+
const promptResults = [];
|
|
1123
|
+
for (const hub of hubs) {
|
|
1124
|
+
const promptPath = path.join(outDir, `upload-${safeSkillKey(hub)}.md`);
|
|
1125
|
+
const promptResult = await writePrompt(absoluteRoot, { ...flags, hub, out: promptPath });
|
|
1126
|
+
promptResults.push({
|
|
1127
|
+
hub,
|
|
1128
|
+
promptPath: typeof promptResult === 'string' ? promptPath : promptResult.outPath
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
774
1131
|
const inspectedAfterManifest = await inspectSkill(absoluteRoot, flags);
|
|
775
|
-
const shouldEmbedSkillMd = Boolean(flags['embed-skill-md']) || (
|
|
1132
|
+
const shouldEmbedSkillMd = Boolean(flags['embed-skill-md']) || (hubs.includes('red') && !flags['no-skill-md']);
|
|
776
1133
|
const skillMdPath = shouldEmbedSkillMd
|
|
777
|
-
? await writeSkillMdRegistryBlock(absoluteRoot, inspectedAfterManifest,
|
|
1134
|
+
? await writeSkillMdRegistryBlock(absoluteRoot, inspectedAfterManifest, hubs.join(','))
|
|
778
1135
|
: null;
|
|
779
1136
|
const disclosurePath = path.join(outDir, 'REGISTRY_DISCLOSURE.md');
|
|
780
1137
|
await writeFile(
|
|
@@ -793,43 +1150,55 @@ async function setupAgentWorkflow(root, flags) {
|
|
|
793
1150
|
out: snippetPath
|
|
794
1151
|
});
|
|
795
1152
|
}
|
|
1153
|
+
const handoffPath = await writeUnifiedHandoff(absoluteRoot, {
|
|
1154
|
+
outDir,
|
|
1155
|
+
hubs,
|
|
1156
|
+
inspected: inspectedAfterManifest,
|
|
1157
|
+
promptResults
|
|
1158
|
+
});
|
|
796
1159
|
|
|
797
1160
|
const warnings = [];
|
|
798
|
-
|
|
1161
|
+
const promptEvents = [];
|
|
799
1162
|
if (!flags['no-track']) {
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
1163
|
+
for (const hub of hubs) {
|
|
1164
|
+
try {
|
|
1165
|
+
const promptEvent = await submitTrackEvent(absoluteRoot, {
|
|
1166
|
+
...flags,
|
|
1167
|
+
hub,
|
|
1168
|
+
event: 'upload_prompt_generated',
|
|
1169
|
+
'event-source': 'cli',
|
|
1170
|
+
'runtime-kind': 'agent-handoff',
|
|
1171
|
+
scenario: flags.scenario || `third-party-skill-upload:${hub}`,
|
|
1172
|
+
'idempotency-key': `upload_prompt_generated:${hub}:${inspectedAfterManifest.package.sha256}`
|
|
1173
|
+
});
|
|
1174
|
+
promptEvents.push({ hub, promptEvent });
|
|
1175
|
+
} catch (error) {
|
|
1176
|
+
warnings.push(`upload_prompt_generated for ${hub} was not recorded: ${error instanceof Error ? error.message : String(error)}`);
|
|
1177
|
+
}
|
|
811
1178
|
}
|
|
812
1179
|
}
|
|
813
1180
|
|
|
814
1181
|
return {
|
|
815
1182
|
ok: true,
|
|
816
|
-
|
|
1183
|
+
hubs,
|
|
817
1184
|
manifestPath: manifestResult.manifestPath,
|
|
818
|
-
|
|
1185
|
+
handoffPath,
|
|
1186
|
+
promptPaths: promptResults,
|
|
819
1187
|
skillMdPath,
|
|
820
1188
|
disclosurePath,
|
|
821
1189
|
snippetPath: snippetResult ? snippetResult.outPath : null,
|
|
822
|
-
|
|
1190
|
+
promptEvents,
|
|
823
1191
|
warnings,
|
|
824
1192
|
next: [
|
|
825
|
-
`
|
|
826
|
-
|
|
1193
|
+
`Use ${path.relative(absoluteRoot, handoffPath)} as the single Agent handoff. The Agent should not ask the user to manually pick registry files.`,
|
|
1194
|
+
'When the user later pastes a Red Skill / ClawHub / SkillHub / Claude / Dify / Coze official prompt, the Agent should identify the platform and merge the matching registry checklist internally.',
|
|
1195
|
+
'Each prepared upload-<hub>.md pins the correct hub/sourceSurface value so platform analytics stay separated, but those files are internal checklists for the Agent.',
|
|
827
1196
|
skillMdPath
|
|
828
1197
|
? `Registry disclosure was also embedded into ${path.relative(absoluteRoot, skillMdPath)} for restrictive hubs.`
|
|
829
1198
|
: `If the hub rejects extra files, rerun with --embed-skill-md to place registry disclosure inside SKILL.md.`,
|
|
830
1199
|
`Use ${path.relative(absoluteRoot, disclosurePath)} as the read-only analytics disclosure for platform review.`,
|
|
831
1200
|
'Do not upload secrets, .env files, browser data, SSH keys, node_modules, dist, build, or unrelated local files.',
|
|
832
|
-
`If the Skill is published, record the public URL with: ${COMMAND_NAME} track . --event hub_upload_succeeded --hub
|
|
1201
|
+
`If the Skill is published, record the public URL with: ${COMMAND_NAME} track . --event hub_upload_succeeded --hub <red|clawhub|skillhub|claude|dify|coze|generic> --platform-skill-url <url>`,
|
|
833
1202
|
snippetResult
|
|
834
1203
|
? `If the runtime supports HTTP/MCP/webhook, adapt ${path.relative(absoluteRoot, snippetPath)} into the Skill runtime.`
|
|
835
1204
|
: 'Runtime snippet was skipped.'
|
|
@@ -858,6 +1227,19 @@ async function main() {
|
|
|
858
1227
|
print(result, flags.json);
|
|
859
1228
|
return;
|
|
860
1229
|
}
|
|
1230
|
+
if (command === 'doctor') {
|
|
1231
|
+
const result = await runDoctor(root, flags);
|
|
1232
|
+
print(flags.json ? result : formatDoctor(result), flags.json);
|
|
1233
|
+
if (!result.ok) {
|
|
1234
|
+
process.exitCode = 1;
|
|
1235
|
+
}
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
if (command === 'verify') {
|
|
1239
|
+
const result = await verifyRegistry(root, flags);
|
|
1240
|
+
print(flags.json ? result : formatVerify(result), flags.json);
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
861
1243
|
if (command === 'setup' || command === 'handoff') {
|
|
862
1244
|
const result = await setupAgentWorkflow(root, flags);
|
|
863
1245
|
if (flags.json) {
|
|
@@ -865,7 +1247,8 @@ async function main() {
|
|
|
865
1247
|
} else {
|
|
866
1248
|
const lines = [
|
|
867
1249
|
`Wrote ${result.manifestPath}`,
|
|
868
|
-
`Wrote ${result.
|
|
1250
|
+
`Wrote ${result.handoffPath}`,
|
|
1251
|
+
...result.promptPaths.map((item) => `Wrote ${item.promptPath}`),
|
|
869
1252
|
`Wrote ${result.disclosurePath}`,
|
|
870
1253
|
result.snippetPath ? `Wrote ${result.snippetPath}` : null,
|
|
871
1254
|
...result.warnings.map((warning) => `Warning: ${warning}`),
|