@xiashe/skill 0.1.5 → 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 CHANGED
@@ -10,6 +10,8 @@ This package is intentionally separate from the full `@xiashe/cli` product CLI a
10
10
 
11
11
  - inspect a local Skill project
12
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
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
@@ -23,8 +25,11 @@ It does not create upload packages, copy source directories, install background
23
25
  ```bash
24
26
  node packages/xiashe-skill-cli/bin/xiashe-skill.mjs --help
25
27
  node packages/xiashe-skill-cli/bin/xiashe-skill.mjs inspect .
28
+ node packages/xiashe-skill-cli/bin/xiashe-skill.mjs doctor .
26
29
  node packages/xiashe-skill-cli/bin/xiashe-skill.mjs setup . --code XS-XXXX-XXXX --hub all
27
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
28
33
  node packages/xiashe-skill-cli/bin/xiashe-skill.mjs attach . --code XS-XXXX-XXXX
29
34
  node packages/xiashe-skill-cli/bin/xiashe-skill.mjs attach . --public-token pub_xxx --skill-id sk_xxx
30
35
  node packages/xiashe-skill-cli/bin/xiashe-skill.mjs prompt . --hub red --source-url https://example.com/my-skill-repo
@@ -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.5';
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');
@@ -49,6 +49,8 @@ Usage:
49
49
  ${COMMAND_NAME} inspect [skill-dir] [--json]
50
50
  ${COMMAND_NAME} setup [skill-dir] --code <dynamic-code> [--hub all|red|clawhub|skillhub|claude|dify|coze|generic]
51
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]
52
54
  ${COMMAND_NAME} attach [skill-dir] --code <dynamic-code> [--name <name>]
53
55
  ${COMMAND_NAME} attach [skill-dir] --public-token <token> [--skill-id <id>] [--name <name>]
54
56
  ${COMMAND_NAME} prompt [skill-dir] --hub <red|clawhub|skillhub|claude|dify|coze|generic> [--source-url <url>] [--out <file>]
@@ -637,8 +639,10 @@ async function uploadPrompt(inspected, flags) {
637
639
  '- 不要新增 postinstall hook、后台常驻进程、隐藏脚本或混淆逻辑。',
638
640
  `- 如果平台要求安全说明,请明确说明这是用户显式发布的 Skill 项目,${MANIFEST_FILE} 只包含公开 registry 归因和可关闭的 analytics 声明。`,
639
641
  `- 如果目标平台支持工具调用、MCP server、webhook 或 Skill runtime HTTP 请求,请在 Skill 被调用时向 ${PRODUCT_NAME} registry 发送聚合事件:skill_invoked / skill_completed / skill_failed。`,
642
+ `- 如果 runtime 没有向 ${registryUrl} 发送事件,${PRODUCT_NAME} 无法看到真实调用量;第三方平台内部显示“已使用”不会自动同步到 ${PRODUCT_NAME}。`,
640
643
  '- 事件里只放公开元数据、聚合状态和简短场景标签,不要发送用户输入原文、私密业务内容、账号凭据或文件内容。',
641
644
  '- installationId 必须是稳定的匿名实例 ID,用于统计唯一安装实例;不要使用邮箱、手机号、真实用户名或设备序列号。',
645
+ '- 发布前请至少发送一次测试事件,并确认 Dashboard 能看到对应 hub/sourceSurface;否则把这个平台标记为“仅上传/手动上报”。',
642
646
  '',
643
647
  `发布成功后,请把公开链接和平台返回的信息回传给${PRODUCT_NAME} registry。只发送公开元数据和聚合事件,不发送用户内容、私密输入或账号凭据。`,
644
648
  '',
@@ -719,6 +723,8 @@ async function writeUnifiedHandoff(root, args) {
719
723
  '',
720
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.',
721
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
+ '',
722
728
  'Best-effort callback placement:',
723
729
  '',
724
730
  '- Send `runtime_install_seen` once when a stable anonymous installation/session is first observed.',
@@ -744,6 +750,14 @@ async function writeUnifiedHandoff(root, args) {
744
750
  '```bash',
745
751
  `${COMMAND_NAME} track . --event hub_upload_succeeded --hub <hub> --platform-skill-url <published-url>`,
746
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.',
747
761
  ''
748
762
  ].join('\n');
749
763
  const handoffPath = path.join(args.outDir, HANDOFF_FILE);
@@ -891,6 +905,209 @@ async function submitTrackEvent(root, flags) {
891
905
  return { ok: true, registryUrl, payload, result };
892
906
  }
893
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
+
894
1111
  async function setupAgentWorkflow(root, flags) {
895
1112
  const hubs = setupHubsFromFlags(flags);
896
1113
  const absoluteRoot = path.resolve(root || '.');
@@ -1010,6 +1227,19 @@ async function main() {
1010
1227
  print(result, flags.json);
1011
1228
  return;
1012
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
+ }
1013
1243
  if (command === 'setup' || command === 'handoff') {
1014
1244
  const result = await setupAgentWorkflow(root, flags);
1015
1245
  if (flags.json) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiashe/skill",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "xiashe-skill": "bin/xiashe-skill.mjs"