@xiashe/skill 0.1.24 → 0.1.26

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
@@ -44,7 +44,9 @@ node packages/xiashe-skill-cli/bin/xiashe-skill.mjs snippet . --target js
44
44
  node packages/xiashe-skill-cli/bin/xiashe-skill.mjs track . --event skill_invoked --hub red --dry-run --json
45
45
  ```
46
46
 
47
- Use the `claimUrl` returned with a dashboard dynamic code exactly as issued. `publish` runs the same preflight automatically before it consumes a code: it verifies the exact registry origin, protocol version, and environment id exposed by that deployment. A dynamic code without an explicit `--claim-url` is rejected before the code is consumed, so development, CN, and global codes cannot accidentally be sent to the production registry. For a resumed upload, the CLI uses the registry origin stored in the local private manifest and preflights it again.
47
+ Use the `claimUrl` returned with a dashboard dynamic code exactly as issued. `publish` runs the same preflight automatically before it consumes a code: it verifies the exact registry origin, protocol version, and environment id exposed by that deployment. A dynamic code without an explicit `--claim-url` is rejected before the code is consumed, so development, CN, and global codes cannot accidentally be sent to the production registry. For an existing Skill update, use the single public `--registry-origin https://<registry-host>` option. The CLI derives its routes, compares that origin and the registry environment id with the local private manifest, and stops before writing or uploading when they differ. Do not copy individual internal endpoint overrides into an update task.
48
+
49
+ When an existing project was originally linked through XiaShe, `@agentpie/skill publish` may adopt its `.xiashe/xiashe.skill.json` only for an update without a dynamic code or explicit public token. The legacy manifest must identify itself as XiaShe and contain its saved registry origin and environment. The CLI verifies both, then writes a separate `.agentpie/agentpie.skill.json`; the XiaShe manifest is retained. A missing value or mismatch stops before a manifest write or package upload, so development, CN, and global Skill identities stay isolated.
48
50
 
49
51
  ## Reliable publish and recovery
50
52
 
@@ -68,7 +68,7 @@ function isTransportError(error) {
68
68
  }
69
69
 
70
70
  const LOCAL_ENV_DEFAULTS = loadLocalEnvDefaults();
71
- const VERSION = process.env.XIASHE_SKILL_CLI_VERSION || '0.1.24';
71
+ const VERSION = process.env.XIASHE_SKILL_CLI_VERSION || '0.1.25';
72
72
  const COMMAND_NAME = process.env.XIASHE_SKILL_CLI_NAME || 'xiashe-skill';
73
73
  const PRODUCT_NAME = process.env.XIASHE_SKILL_PRODUCT_NAME || (COMMAND_NAME === 'agentpie-skill' ? 'AgentPie' : 'XiaShe');
74
74
  const REGISTRY_PROVIDER = process.env.XIASHE_SKILL_REGISTRY_PROVIDER || (COMMAND_NAME === 'agentpie-skill' ? 'agentpie' : 'xiashe');
@@ -97,6 +97,14 @@ const MANIFEST_FILE = process.env.XIASHE_SKILL_MANIFEST_FILE || (COMMAND_NAME ==
97
97
  ? 'agentpie.skill.json'
98
98
  : 'xiashe.skill.json');
99
99
  const WORK_DIR = process.env.XIASHE_SKILL_WORK_DIR || (COMMAND_NAME === 'agentpie-skill' ? '.agentpie' : '.xiashe');
100
+ // A Skill that was first linked through XiaShe may later be updated through
101
+ // AgentPie. AgentPie can adopt that local link only for an update, never for a
102
+ // new claim. The registry origin and environment are still checked before we
103
+ // write the AgentPie manifest or upload anything.
104
+ const CAN_ADOPT_LEGACY_XIASHE_MANIFEST = REGISTRY_PROVIDER === 'agentpie' &&
105
+ process.env.XIASHE_SKILL_LEGACY_MANIFEST_COMPAT !== '0';
106
+ const LEGACY_XIASHE_WORK_DIR = '.xiashe';
107
+ const LEGACY_XIASHE_MANIFEST_FILE = 'xiashe.skill.json';
100
108
  const PUBLIC_PROTOCOL_DIR = process.env.XIASHE_SKILL_PUBLIC_PROTOCOL_DIR || (REGISTRY_PROVIDER === 'agentpie' ? 'agentpie' : 'xiashe');
101
109
  const PUBLIC_PACKAGE_MANIFEST_FILE = 'package-manifest.json';
102
110
  const HANDOFF_FILE = 'UPLOAD_HANDOFF.md';
@@ -305,6 +313,12 @@ const DEFAULT_IGNORE = new Set([
305
313
  const MAX_FILE_BYTES = 2 * 1024 * 1024;
306
314
  const MAX_SOURCE_BYTES = 30 * 1024 * 1024;
307
315
  const MAX_XIASHE_PACKAGE_BYTES = 100 * 1024 * 1024;
316
+ // Update tasks are frequently run from a chat workspace that contains the
317
+ // original Skill folder one level below the current directory. Look only in a
318
+ // small, project-local area: this is helpful without turning a publish command
319
+ // into a machine-wide search for credentials or manifests.
320
+ const MAX_LOCAL_LINK_DISCOVERY_DEPTH = 4;
321
+ const MAX_LOCAL_LINK_DISCOVERY_DIRECTORIES = 160;
308
322
  const gzipAsync = promisify(gzip);
309
323
 
310
324
  function usage() {
@@ -316,7 +330,7 @@ Usage:
316
330
  ${COMMAND_NAME} setup [skill-dir] --code <dynamic-code> [--hub all|${FIRST_PARTY_HUB}|red]
317
331
  ${COMMAND_NAME} setup [skill-dir] --public-token <token> --skill-id <id> [--hub all|${FIRST_PARTY_HUB}|red]
318
332
  ${COMMAND_NAME} doctor [skill-dir] [--json]
319
- ${COMMAND_NAME} preflight --claim-url <url> [--json]
333
+ ${COMMAND_NAME} preflight (--claim-url <url> | --registry-origin <url>) [--json]
320
334
  ${COMMAND_NAME} verify [skill-dir] [--hub <hub>] [--scenario <label>] [--dry-run]
321
335
  ${COMMAND_NAME} attach [skill-dir] --code <dynamic-code> [--name <name>]
322
336
  ${COMMAND_NAME} attach [skill-dir] --public-token <token> [--skill-id <id>] [--name <name>]
@@ -327,6 +341,7 @@ Usage:
327
341
  Options:
328
342
  --public-token <token> Public write token issued by ${PRODUCT_NAME} registry.
329
343
  --code <dynamic-code> One-time ${PRODUCT_NAME} dashboard code used to claim registry identity.
344
+ --registry-origin <url> Existing Skill update origin. Derives all registry routes and verifies the saved environment.
330
345
  --claim-url <url> Code claim endpoint. Defaults to XIASHE_SKILL_CLAIM_URL or ${DEFAULT_CLAIM_URL}
331
346
  --registry-doctor-url <url> Registry preflight endpoint. Defaults to the claim URL origin.
332
347
  --skill-id <id> Public ${PRODUCT_NAME} Skill registry id.
@@ -525,6 +540,10 @@ function latestVersionEndpoint(inspectedOrRegistry = {}) {
525
540
  const skillKey = safeSkillKey(inspectedOrRegistry.skillKey || registry.skillKey || '');
526
541
  const creatorCardUrl = normalizeText(registry.creatorCardUrl || registry.creatorProfile?.cardUrl, 800);
527
542
  const publicSkillId = normalizeText(registry.skillId || registry.publicSkillId, 160);
543
+ // Existing Skills must keep checking the registry that originally issued
544
+ // their manifest. Falling back to a CLI default here would make a dev
545
+ // package discover Global/CN updates after a machine-level config change.
546
+ const registryOrigin = normalizeBaseUrl(registry.registryOrigin) || DEFAULT_BASE_URL;
528
547
  const params = new URLSearchParams();
529
548
  if (publicSkillId) {
530
549
  params.set('publicSkillId', publicSkillId);
@@ -532,7 +551,7 @@ function latestVersionEndpoint(inspectedOrRegistry = {}) {
532
551
  params.set('skillKey', skillKey);
533
552
  params.set('creatorCardUrl', creatorCardUrl);
534
553
  }
535
- return `${DEFAULT_BASE_URL}/registry/skill/latest${params.toString() ? `?${params.toString()}` : ''}`;
554
+ return `${registryOrigin}/registry/skill/latest${params.toString() ? `?${params.toString()}` : ''}`;
536
555
  }
537
556
 
538
557
  function creatorFooterTemplate(inspected, language = 'zh') {
@@ -808,12 +827,51 @@ async function readJsonFile(filePath) {
808
827
  }
809
828
  }
810
829
 
830
+ function localManifestPath(root) {
831
+ return path.join(root, WORK_DIR, MANIFEST_FILE);
832
+ }
833
+
834
+ function legacyXiaSheManifestPath(root) {
835
+ if (!CAN_ADOPT_LEGACY_XIASHE_MANIFEST) return null;
836
+ return path.join(root, LEGACY_XIASHE_WORK_DIR, LEGACY_XIASHE_MANIFEST_FILE);
837
+ }
838
+
839
+ function canAdoptLegacyXiaSheManifest(flags = {}) {
840
+ // A dynamic code or a supplied public token starts/resumes an explicit
841
+ // identity flow. Never let it silently attach to a legacy local identity.
842
+ return CAN_ADOPT_LEGACY_XIASHE_MANIFEST &&
843
+ !normalizeText(flags.code, 80) &&
844
+ !normalizeText(flags['public-token'], 512) &&
845
+ (!flags['manifest-path'] || Boolean(flags['local-manifest']));
846
+ }
847
+
848
+ async function readSkillManifest(root, flags = {}) {
849
+ const explicitManifestPath = normalizeText(flags['manifest-path'], 2_000);
850
+ const legacyManifestPath = canAdoptLegacyXiaSheManifest(flags)
851
+ ? legacyXiaSheManifestPath(root)
852
+ : null;
853
+ const candidates = [
854
+ explicitManifestPath ? path.resolve(explicitManifestPath) : path.join(root, MANIFEST_FILE),
855
+ localManifestPath(root),
856
+ legacyManifestPath
857
+ ].filter(Boolean);
858
+ const uniqueCandidates = [...new Set(candidates)];
859
+
860
+ for (const manifestPath of uniqueCandidates) {
861
+ const manifest = await readJsonFile(manifestPath);
862
+ if (!manifest) continue;
863
+ return {
864
+ manifest,
865
+ manifestPath,
866
+ source: manifestPath === legacyManifestPath ? 'legacy_xiashe' : 'current'
867
+ };
868
+ }
869
+ return { manifest: null, manifestPath: null, source: null };
870
+ }
871
+
811
872
  async function inferSkillMetadata(root, flags = {}) {
812
- const manifestPath = flags['manifest-path']
813
- ? path.resolve(flags['manifest-path'])
814
- : path.join(root, MANIFEST_FILE);
815
- const localManifestPath = path.join(root, WORK_DIR, MANIFEST_FILE);
816
- const manifest = await readJsonFile(manifestPath) || await readJsonFile(localManifestPath);
873
+ const manifestLookup = await readSkillManifest(root, flags);
874
+ const manifest = manifestLookup.manifest;
817
875
  const packageJson = await readJsonFile(path.join(root, 'package.json'));
818
876
  const skillMdPath = path.join(root, 'SKILL.md');
819
877
  const readmePath = path.join(root, 'README.md');
@@ -835,10 +893,113 @@ async function inferSkillMetadata(root, flags = {}) {
835
893
  billingModel: normalizeText(flags['billing-model'] || manifest?.billingModel || manifest?.registry?.billingModel, 80) || null,
836
894
  monetizationMode: normalizeText(flags['monetization-mode'] || manifest?.monetizationMode || manifest?.registry?.monetizationMode, 80) || null,
837
895
  accessMode: normalizeText(flags['access-mode'] || manifest?.accessMode || manifest?.registry?.accessMode, 80) || null,
896
+ manifestPath: manifestLookup.manifestPath,
897
+ manifestSource: manifestLookup.source,
838
898
  registry: manifest?.registry || null
839
899
  };
840
900
  }
841
901
 
902
+ function hasSkillProjectMarker(root) {
903
+ return ['SKILL.md', 'package.json', 'README.md'].some((fileName) => existsSync(path.join(root, fileName)));
904
+ }
905
+
906
+ async function readExistingSkillLink(root, flags = {}) {
907
+ const manifestLookup = await readSkillManifest(root, flags);
908
+ const manifest = manifestLookup.manifest;
909
+ const registry = manifest?.registry;
910
+ const publicToken = normalizeText(registry?.publicToken, 512);
911
+ const publicSkillId = normalizeText(registry?.publicSkillId || registry?.skillId, 160);
912
+ if (!manifest || !registry || (!publicToken && !publicSkillId)) return null;
913
+ return {
914
+ root,
915
+ manifestPath: manifestLookup.manifestPath,
916
+ manifestSource: manifestLookup.source,
917
+ manifest,
918
+ skillKey: safeSkillKey(manifest.skillKey || registry.skillKey || manifest.name || path.basename(root))
919
+ };
920
+ }
921
+
922
+ async function resolveExistingSkillRoot(root, flags = {}) {
923
+ const requestedRoot = path.resolve(root || '.');
924
+ const info = await stat(requestedRoot).catch(() => null);
925
+ if (!info?.isDirectory()) {
926
+ throw new Error(`Skill directory not found: ${requestedRoot}`);
927
+ }
928
+
929
+ // A fresh claim or an explicit manifest path is intentional. Do not replace
930
+ // either with automatic discovery, which could attach the wrong Skill.
931
+ if (flags.code || flags['public-token'] || flags['manifest-path']) {
932
+ return { root: requestedRoot, discovered: false };
933
+ }
934
+
935
+ const directLink = await readExistingSkillLink(requestedRoot, flags);
936
+ if (directLink) return { root: requestedRoot, discovered: false };
937
+
938
+ const requestedRootHasMarker = hasSkillProjectMarker(requestedRoot);
939
+ const expectedSkillKey = normalizeText(flags['skill-key'], 120)
940
+ ? safeSkillKey(flags['skill-key'])
941
+ : requestedRootHasMarker
942
+ ? (await inferSkillMetadata(requestedRoot, flags)).skillKey
943
+ : '';
944
+ const candidates = [];
945
+ const candidateRoots = new Set();
946
+
947
+ const addCandidate = async (candidateRoot) => {
948
+ const canonicalRoot = path.resolve(candidateRoot);
949
+ if (candidateRoots.has(canonicalRoot) || !hasSkillProjectMarker(canonicalRoot)) return;
950
+ candidateRoots.add(canonicalRoot);
951
+ const candidate = await readExistingSkillLink(canonicalRoot, flags);
952
+ if (!candidate) return;
953
+ if (expectedSkillKey && candidate.skillKey !== expectedSkillKey) return;
954
+ candidates.push(candidate);
955
+ };
956
+
957
+ // Only walk upward when the requested folder identifies the Skill. Without
958
+ // that key, an unrelated parent workspace with one linked Skill could be
959
+ // selected accidentally. A generic chat workspace is handled by the bounded
960
+ // child search below instead.
961
+ if (expectedSkillKey) {
962
+ let ancestor = path.dirname(requestedRoot);
963
+ for (let index = 0; index < MAX_LOCAL_LINK_DISCOVERY_DEPTH; index += 1) {
964
+ if (ancestor === path.dirname(ancestor)) break;
965
+ await addCandidate(ancestor);
966
+ ancestor = path.dirname(ancestor);
967
+ }
968
+ }
969
+
970
+ // Then look through a small subtree, covering the common case where a chat
971
+ // task attaches the original Skill directory as a child of its workspace.
972
+ const queue = [{ directory: requestedRoot, depth: 0 }];
973
+ const visited = new Set();
974
+ while (queue.length > 0 && visited.size < MAX_LOCAL_LINK_DISCOVERY_DIRECTORIES) {
975
+ const { directory, depth } = queue.shift();
976
+ const canonicalDirectory = path.resolve(directory);
977
+ if (visited.has(canonicalDirectory)) continue;
978
+ visited.add(canonicalDirectory);
979
+ if (depth > 0) await addCandidate(canonicalDirectory);
980
+ if (depth >= MAX_LOCAL_LINK_DISCOVERY_DEPTH) continue;
981
+ const entries = await readdir(canonicalDirectory, { withFileTypes: true }).catch(() => []);
982
+ for (const entry of entries) {
983
+ if (!entry.isDirectory() || shouldIgnore(entry.name)) continue;
984
+ queue.push({ directory: path.join(canonicalDirectory, entry.name), depth: depth + 1 });
985
+ }
986
+ }
987
+
988
+ if (candidates.length === 1) {
989
+ return { root: candidates[0].root, discovered: true };
990
+ }
991
+ if (candidates.length > 1) {
992
+ throw new Error(
993
+ 'CREATOR_SKILL_MANIFEST_AMBIGUOUS: more than one matching local Skill connection was found. ' +
994
+ 'The CLI did not update or create any Skill. Run the command from the intended Skill folder.'
995
+ );
996
+ }
997
+ throw new Error(
998
+ 'CREATOR_SKILL_MANIFEST_NOT_FOUND: no matching local Skill connection was found in this project. ' +
999
+ 'The CLI did not update or create any Skill. Run the command from the original Skill folder.'
1000
+ );
1001
+ }
1002
+
842
1003
  function shouldIgnore(name) {
843
1004
  return DEFAULT_IGNORE.has(name) ||
844
1005
  name.startsWith('.env.') ||
@@ -969,6 +1130,49 @@ function claimUrlForExistingManifest(flags = {}, registry = {}) {
969
1130
  );
970
1131
  }
971
1132
 
1133
+ function normalizeRegistryOrigin(value) {
1134
+ const raw = normalizeText(value, 1_000);
1135
+ if (!raw) return '';
1136
+ let url;
1137
+ try {
1138
+ url = new URL(raw);
1139
+ } catch {
1140
+ throw new Error('CREATOR_SKILL_REGISTRY_ORIGIN_INVALID: registry origin is not a valid URL.');
1141
+ }
1142
+ const host = url.hostname.toLowerCase();
1143
+ const isLocalhost = host === 'localhost' || host === '127.0.0.1' || host === '::1' || host.endsWith('.localhost');
1144
+ if (url.pathname !== '/' || url.search || url.hash || url.username || url.password) {
1145
+ throw new Error('CREATOR_SKILL_REGISTRY_ORIGIN_INVALID: registry origin must not contain a path, query, fragment, or credentials.');
1146
+ }
1147
+ if (url.protocol !== 'https:' && !(isLocalhost && process.env.XIASHE_SKILL_ALLOW_INSECURE_LOCALHOST === '1')) {
1148
+ throw new Error('CREATOR_SKILL_REGISTRY_ORIGIN_INSECURE: only HTTPS registry origins are accepted.');
1149
+ }
1150
+ return url.origin.replace(/\/+$/, '');
1151
+ }
1152
+
1153
+ function registryOriginFromFlags(flags = {}) {
1154
+ return normalizeRegistryOrigin(flags['registry-origin']);
1155
+ }
1156
+
1157
+ function applyRegistryOriginFlags(flags = {}) {
1158
+ const registryOrigin = registryOriginFromFlags(flags);
1159
+ if (!registryOrigin) return flags;
1160
+ if (normalizeText(flags.code, 80)) {
1161
+ throw new Error('CREATOR_SKILL_REGISTRY_ORIGIN_WITH_CODE_FORBIDDEN: dynamic codes must use the exact dashboard --claim-url, not --registry-origin.');
1162
+ }
1163
+ for (const key of ['claim-url', 'registry-url', 'agent-ack-url', 'registry-doctor-url']) {
1164
+ if (normalizeText(flags[key], 1_000)) {
1165
+ throw new Error(`CREATOR_SKILL_REGISTRY_ORIGIN_CONFLICT: --registry-origin cannot be combined with --${key}.`);
1166
+ }
1167
+ }
1168
+ return {
1169
+ ...flags,
1170
+ 'claim-url': `${registryOrigin}/registry/skill/claim`,
1171
+ 'registry-url': `${registryOrigin}/registry/skill-events`,
1172
+ 'agent-ack-url': `${registryOrigin}/registry/agent-ack`
1173
+ };
1174
+ }
1175
+
972
1176
  function configuredRegistryOrigins() {
973
1177
  const supplied = [
974
1178
  process.env.XIASHE_SKILL_TRUSTED_REGISTRY_ORIGINS,
@@ -1509,15 +1713,42 @@ async function writeManifest(root, flags) {
1509
1713
  const inspected = await inspectSkill(root, flags);
1510
1714
  const code = normalizeText(flags.code, 80);
1511
1715
  const existingRegistry = inspected.registry || {};
1716
+ const savedRegistryOrigin = normalizeBaseUrl(existingRegistry.registryOrigin);
1717
+ const savedEnvironmentId = normalizeText(existingRegistry.environmentId, 160);
1718
+ const requestedRegistryOrigin = registryOriginFromFlags(flags);
1512
1719
  const suppliedPublicToken = normalizeText(flags['public-token'], 512);
1720
+ const isLegacyXiaSheMigration = inspected.manifestSource === 'legacy_xiashe';
1721
+ if (
1722
+ isLegacyXiaSheMigration &&
1723
+ (existingRegistry.provider !== 'xiashe' || !savedRegistryOrigin || !savedEnvironmentId)
1724
+ ) {
1725
+ throw new Error(
1726
+ 'CREATOR_SKILL_LEGACY_MANIFEST_UNVERIFIABLE: this XiaShe local connection does not contain a verifiable registry origin and environment. ' +
1727
+ 'No AgentPie manifest was written. Update from the original XiaShe connection or attach the Skill explicitly.'
1728
+ );
1729
+ }
1513
1730
  if (!code && suppliedPublicToken && !explicitClaimUrl(flags) && !claimUrlFromRegistryOrigin(existingRegistry.registryOrigin)) {
1514
1731
  throw new Error(
1515
1732
  'CREATOR_SKILL_CLAIM_URL_REQUIRED: a new manifest created from --public-token must include --claim-url. ' +
1516
1733
  'Existing manifests resume through their saved registry origin.'
1517
1734
  );
1518
1735
  }
1736
+ if (!code && !savedRegistryOrigin && !requestedRegistryOrigin) {
1737
+ throw new Error(
1738
+ 'CREATOR_SKILL_REGISTRY_ORIGIN_REQUIRED: existing Skill updates must include --registry-origin so the CLI can verify the original environment before writing or uploading.'
1739
+ );
1740
+ }
1741
+ if (!code && savedRegistryOrigin && requestedRegistryOrigin && savedRegistryOrigin !== requestedRegistryOrigin) {
1742
+ throw new Error('CREATOR_SKILL_REGISTRY_ENVIRONMENT_MISMATCH: the supplied registry origin differs from the original Skill manifest. No files were changed.');
1743
+ }
1519
1744
  const claimUrl = code ? claimUrlForCode(flags) : claimUrlForExistingManifest(flags, existingRegistry);
1520
- const registryPreflight = code ? await preflightRegistryClaim(claimUrl, flags) : null;
1745
+ const registryPreflight = await preflightRegistryClaim(claimUrl, flags);
1746
+ if (!code && savedRegistryOrigin && savedRegistryOrigin !== registryPreflight.registryOrigin) {
1747
+ throw new Error('CREATOR_SKILL_REGISTRY_ENVIRONMENT_MISMATCH: the saved Skill manifest belongs to a different registry origin. No files were changed.');
1748
+ }
1749
+ if (!code && savedEnvironmentId && savedEnvironmentId !== registryPreflight.environmentId) {
1750
+ throw new Error('CREATOR_SKILL_REGISTRY_ENVIRONMENT_MISMATCH: the saved Skill manifest belongs to a different registry environment. No files were changed.');
1751
+ }
1521
1752
  const claim = code
1522
1753
  ? await postJson(
1523
1754
  registryPreflight.claimUrl,
@@ -1583,9 +1814,9 @@ async function writeManifest(root, flags) {
1583
1814
  accessMode: normalizeText(flags['access-mode'] || inspected.accessMode || existingRegistry.accessMode, 80) || null,
1584
1815
  registry: {
1585
1816
  provider: REGISTRY_PROVIDER,
1586
- protocolVersion: normalizeText(claim?.registry?.protocolVersion || registryPreflight?.protocolVersion, 120) || null,
1587
- environmentId: normalizeText(claim?.registry?.environmentId || registryPreflight?.environmentId, 160) || null,
1588
- registryOrigin: normalizeBaseUrl(claim?.registry?.registryOrigin || registryPreflight?.registryOrigin) || null,
1817
+ protocolVersion: normalizeText(claim?.registry?.protocolVersion || registryPreflight?.protocolVersion || existingRegistry.protocolVersion, 120) || null,
1818
+ environmentId: normalizeText(claim?.registry?.environmentId || registryPreflight?.environmentId || existingRegistry.environmentId, 160) || null,
1819
+ registryOrigin: normalizeBaseUrl(claim?.registry?.registryOrigin || registryPreflight?.registryOrigin || existingRegistry.registryOrigin) || null,
1589
1820
  skillId: normalizeText(flags['skill-id'] || claim?.skillId || existingRegistry.skillId, 160) || null,
1590
1821
  publicToken,
1591
1822
  isPaid: Boolean(flags['paid-skill'] || flags['protected-skill'] || inspected.isPaid || existingRegistry.isPaid || existingRegistry.paidAccess),
@@ -1593,8 +1824,8 @@ async function writeManifest(root, flags) {
1593
1824
  billingModel: normalizeText(flags['billing-model'] || inspected.billingModel || existingRegistry.billingModel, 80) || null,
1594
1825
  monetizationMode: normalizeText(flags['monetization-mode'] || inspected.monetizationMode || existingRegistry.monetizationMode, 80) || null,
1595
1826
  accessMode: normalizeText(flags['access-mode'] || inspected.accessMode || existingRegistry.accessMode, 80) || null,
1596
- registryUrl: normalizeText(flags['registry-url'] || claim?.registryUrl || existingRegistry.registryUrl, 800) || DEFAULT_REGISTRY_URL,
1597
- agentAckUrl: normalizeText(flags['agent-ack-url'] || existingRegistry.agentAckUrl, 800) || DEFAULT_AGENT_ACK_URL,
1827
+ registryUrl: normalizeText(flags['registry-url'] || claim?.registryUrl || existingRegistry.registryUrl, 800) || `${registryPreflight.registryOrigin}/registry/skill-events`,
1828
+ agentAckUrl: normalizeText(flags['agent-ack-url'] || existingRegistry.agentAckUrl, 800) || `${registryPreflight.registryOrigin}/registry/agent-ack`,
1598
1829
  creatorCardUrl,
1599
1830
  creatorProfile,
1600
1831
  eventSchemaVersion: EVENT_SCHEMA_VERSION,
@@ -1642,7 +1873,7 @@ async function writeManifest(root, flags) {
1642
1873
  sourceSurface: hub,
1643
1874
  skillKey: safeSkillKey(claim?.skillKey || inspected.skillKey),
1644
1875
  publicSkillId: normalizeText(flags['skill-id'] || claim?.skillId || existingRegistry.skillId, 160) || null,
1645
- agentAckUrl: normalizeText(flags['agent-ack-url'] || existingRegistry.agentAckUrl, 800) || DEFAULT_AGENT_ACK_URL,
1876
+ agentAckUrl: normalizeText(flags['agent-ack-url'] || existingRegistry.agentAckUrl, 800) || `${registryPreflight.registryOrigin}/registry/agent-ack`,
1646
1877
  creatorCardUrl,
1647
1878
  creatorProfile
1648
1879
  }
@@ -1667,6 +1898,7 @@ async function writeManifest(root, flags) {
1667
1898
  ok: true,
1668
1899
  manifestPath,
1669
1900
  manifest,
1901
+ adoptedLegacyXiaSheManifest: inspected.manifestSource === 'legacy_xiashe',
1670
1902
  claimPackageArtifactUpload: claim?.packageArtifactUpload ?? null,
1671
1903
  registryPreflight
1672
1904
  };
@@ -1948,6 +2180,10 @@ function redPublicTrackingContractBlock(inspected) {
1948
2180
  `- Latest version endpoint: ${updateEndpoint}`,
1949
2181
  '- 如果联网可用,首次使用前或每日第一次使用前检查一次。',
1950
2182
  '- 如果 latestVersion 与 Current version 不同,先提示用户有新版本,并询问是否更新;未经确认,不要修改本地文件。',
2183
+ ...(protectedPaidSkill ? [
2184
+ `- 对这个受保护包,公开 latest version endpoint 只能用于发现更新,不能用于下载。先通过 ${PRODUCT_NAME} Gateway 调用 \`xiashe.check_skill_update\`;用户明确同意后,调用 \`xiashe.get_skill_update\` 并传 \`userConfirmed=true\`。`,
2185
+ '- 更新交付到当前 Agent 的一次性私有地址:先保留旧包备份,在临时目录下载、校验 SHA-256、解压并阅读 SKILL.md,全部成功后再原子替换;任一步失败都保留旧包。已购账户更新不再次收费。'
2186
+ ] : []),
1951
2187
  '- 如果 endpoint 返回 blocked / paused / disabled,提示当前版本已停用,不要静默继续。',
1952
2188
  '- 如果无法联网,继续完成用户任务,并简单说明更新状态未检查。',
1953
2189
  '',
@@ -2000,6 +2236,10 @@ function skillMdRegistryBlock(inspected, hub) {
2000
2236
  `- Latest version endpoint: ${updateEndpoint}`,
2001
2237
  '- When network access is available, check the latest version endpoint before first use or once per day.',
2002
2238
  '- If a newer latestVersion is returned, show it and ask the user before changing local files.',
2239
+ ...(protectedPaidSkill ? [
2240
+ `- For this protected package, the public latest-version endpoint is discovery-only and must not be used as a download URL. Call ${PRODUCT_NAME} Gateway \`xiashe.check_skill_update\`, then after explicit approval call \`xiashe.get_skill_update\` with \`userConfirmed=true\`.`,
2241
+ '- Keep the installed package as backup; download the one-time private delivery into a temporary directory, verify SHA-256, unpack and read SKILL.md, then atomically replace. Keep the old package on any failure. An entitled account is not charged again for updates.'
2242
+ ] : []),
2003
2243
  '- If the endpoint returns blocked / paused / disabled, tell the user this installed version is disabled and do not continue silently.',
2004
2244
  '- If the network is unavailable, continue the user task and briefly state that update status was not checked.',
2005
2245
  '',
@@ -2100,6 +2340,7 @@ function buildRuntimeYaml(inspected, hub = 'red') {
2100
2340
  " when: 'before_first_use_or_daily_when_network_available'",
2101
2341
  " noNetwork: 'continue_and_state_update_status_unchecked'",
2102
2342
  " updateAvailable: 'show_latest_version_and_ask_user_before_changing_files'",
2343
+ ` protectedDelivery: ${yamlString(protectedPaidSkill ? 'gateway_check_then_confirmed_one_time_delivery; backup_verify_extract_read_skill_md_then_atomic_replace; no_second_charge_for_entitled_account' : 'not_required')}`,
2103
2344
  " blocked: 'tell_user_current_version_is_disabled_and_do_not_continue_silently'",
2104
2345
  'creator:',
2105
2346
  ` displayName: ${yamlString(creatorName)}`,
@@ -2212,6 +2453,7 @@ async function writePublicProtocolFiles(root, inspected, hub = 'red') {
2212
2453
  const disclosurePath = path.join(protocolDir, 'REGISTRY_DISCLOSURE.md');
2213
2454
  const runtimePath = path.join(protocolDir, 'runtime.yaml');
2214
2455
  const registry = inspected.registry || {};
2456
+ const protectedPaidSkill = isProtectedPaidSkill(inspected);
2215
2457
  const publicSkillId = registry.skillId || registry.publicSkillId || `<public skill id from ${PRODUCT_NAME} registry>`;
2216
2458
  const agentAck = [
2217
2459
  agentAckInstructionBlock(inspected, hub),
@@ -2220,7 +2462,9 @@ async function writePublicProtocolFiles(root, inspected, hub = 'red') {
2220
2462
  '',
2221
2463
  '1. Install the Skill through the target platform official flow.',
2222
2464
  `2. Read \`${PUBLIC_PROTOCOL_DIR}/runtime.yaml\` and this file to get the exact \`publicSkillId\`: \`${publicSkillId}\`. Never infer it from the title or directory name.`,
2223
- '3. When network is available, check `runtime.yaml` `updateCheck.latestVersionEndpoint` before first use or once per day. If a newer version exists, ask the user whether to update before modifying local files.',
2465
+ protectedPaidSkill
2466
+ ? `3. When network is available, check \`runtime.yaml\` \`updateCheck.latestVersionEndpoint\` before first use or once per day. If a newer version exists, use ${PRODUCT_NAME} Gateway \`xiashe.check_skill_update\`, ask the user, then call \`xiashe.get_skill_update\` with \`userConfirmed=true\`. Preserve the old local package until the new private archive is verified, unpacked, and its SKILL.md read.`
2467
+ : '3. When network is available, check `runtime.yaml` `updateCheck.latestVersionEndpoint` before first use or once per day. If a newer version exists, ask the user whether to update before modifying local files.',
2224
2468
  '4. Send `installed` once after installation succeeds. If first use happens before the install event, backfill `installed` first with the same anonymous `installationId`.',
2225
2469
  '5. Send `used` when the Skill starts, then `completed` or `failed` when the task ends.',
2226
2470
  '6. End every completed Skill turn with the localized footer from `runtime.yaml`. Use the recommendation slot only when the Ack response returns one.',
@@ -2602,7 +2846,9 @@ async function writeUnifiedHandoff(root, args) {
2602
2846
  `Event schema: ${registry.eventSchemaVersion || EVENT_SCHEMA_VERSION}`,
2603
2847
  '',
2604
2848
  `The simplest cross-platform runtime path is Agent Ack. For Red Skill, the host Agent should read ${PUBLIC_PROTOCOL_DIR}/AGENT_ACK.md or private runtime config first, then report \`installed\`, \`used\`, \`completed\`, or \`failed\` with publicSkillId, anonymous installationId, and invocationId. Do not paste endpoint/publicSkillId into marketing descriptions, screenshots, or user-visible replies. No public token or account login is required for Agent Ack.`,
2605
- `For user-side updates, the simplest path is prompt-based: read ${PUBLIC_PROTOCOL_DIR}/runtime.yaml \`updateCheck.latestVersionEndpoint\` or the SKILL.md latest version endpoint before first use/daily use; if a newer version exists, show it and ask the user before changing local files. If blocked, do not continue silently.`,
2849
+ protectedPaidSkill
2850
+ ? `For user-side protected updates, use ${PUBLIC_PROTOCOL_DIR}/runtime.yaml \`updateCheck.latestVersionEndpoint\` only to discover the latest version. The Agent must call ${PRODUCT_NAME} Gateway \`xiashe.check_skill_update\`, show the version and get explicit user approval, then call \`xiashe.get_skill_update\` with \`userConfirmed=true\`. Its one-time archive must be verified, unpacked, and read before atomically replacing the local package; keep the old package on failure. Do not use a public registry link as a package download or charge an entitled account again.`
2851
+ : `For user-side updates, the simplest path is prompt-based: read ${PUBLIC_PROTOCOL_DIR}/runtime.yaml \`updateCheck.latestVersionEndpoint\` or the SKILL.md latest version endpoint before first use/daily use; if a newer version exists, show it and ask the user before changing local files. If blocked, do not continue silently.`,
2606
2852
  'The creator card link is public attribution and should remain visible in allowed platform instructions and user-facing Skill replies. It is not a secret and should not be replaced by registry ids or tokens.',
2607
2853
  `Before sending Agent Ack, read ${PUBLIC_PROTOCOL_DIR}/AGENT_ACK.md, ${WORK_DIR}/AGENT_ACK.md, or the private registry manifest and use the exact \`publicSkillId\`. Never guess it from the Skill title, package name, directory name, or marketplace slug.`,
2608
2854
  'For `installationId`, use a stable anonymous value for this installed copy/runtime connection. If the platform does not provide one, hash `publicSkillId + platform name + install directory or session id`, store only the opaque hash locally, and never send raw paths, emails, phone numbers, real names, prompts, or account identifiers.',
@@ -2895,12 +3141,11 @@ function doctorCheck(id, label, status, message, fix = '') {
2895
3141
 
2896
3142
  async function runDoctor(root, flags) {
2897
3143
  const inspected = await inspectSkill(root, flags);
3144
+ const manifestLookup = await readSkillManifest(inspected.root, flags);
2898
3145
  const hub = normalizeHub(flags.hub || flags.to || 'generic') || 'generic';
2899
3146
  const redHub = hub === 'red';
2900
3147
  const skillMdPath = path.join(inspected.root, 'SKILL.md');
2901
3148
  const readmePath = await findReadmePath(inspected.root);
2902
- const manifestPath = path.join(inspected.root, MANIFEST_FILE);
2903
- const localManifestPath = path.join(inspected.root, WORK_DIR, MANIFEST_FILE);
2904
3149
  const disclosurePath = path.join(inspected.root, WORK_DIR, 'REGISTRY_DISCLOSURE.md');
2905
3150
  const publicAgentAckPath = path.join(inspected.root, PUBLIC_PROTOCOL_DIR, 'AGENT_ACK.md');
2906
3151
  const publicDisclosurePath = path.join(inspected.root, PUBLIC_PROTOCOL_DIR, 'REGISTRY_DISCLOSURE.md');
@@ -2926,9 +3171,7 @@ async function runDoctor(root, flags) {
2926
3171
  scannedCallbackText += `\n${text}`;
2927
3172
  }
2928
3173
  }
2929
- const manifest = inspected.registry
2930
- ? await readJsonFile(manifestPath).catch(() => null) || await readJsonFile(localManifestPath).catch(() => null)
2931
- : null;
3174
+ const manifest = inspected.registry ? manifestLookup.manifest : null;
2932
3175
  const textBundle = `${skillMd}\n${readme}`;
2933
3176
  const hasManifest = Boolean(manifest);
2934
3177
  const registry = manifest?.registry || inspected.registry || {};
@@ -2949,7 +3192,15 @@ async function runDoctor(root, flags) {
2949
3192
  ? doctorCheck('registry_manifest', 'Registry manifest', 'pass', `Found registry manifest for ${registry.provider || REGISTRY_PROVIDER}.`)
2950
3193
  : redHub
2951
3194
  ? doctorCheck('registry_manifest', 'Registry manifest', 'warn', `No local registry manifest found. This does not block Red Skill safety review, but ${PRODUCT_NAME} Agent Ack reporting will be unavailable until setup runs.`, `Run ${COMMAND_NAME} setup . --code <dashboard-code> --hub red if ${PRODUCT_NAME} tracking is needed.`)
2952
- : 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>.`),
3195
+ : doctorCheck(
3196
+ 'registry_manifest',
3197
+ 'Registry manifest',
3198
+ 'fail',
3199
+ CAN_ADOPT_LEGACY_XIASHE_MANIFEST
3200
+ ? `Missing usable registry manifest (${MANIFEST_FILE}, ${WORK_DIR}/${MANIFEST_FILE}, or a compatible XiaShe local manifest).`
3201
+ : `Missing usable registry manifest (${MANIFEST_FILE} or ${WORK_DIR}/${MANIFEST_FILE}).`,
3202
+ `Run ${COMMAND_NAME} setup . --code <dashboard-code> or ${COMMAND_NAME} attach . --public-token <token> --skill-id <id>.`
3203
+ ),
2953
3204
  registryBlockPresent(skillMd)
2954
3205
  ? doctorCheck('registry_block', 'Registry block', 'pass', 'SKILL.md contains the explicit registry disclosure block.')
2955
3206
  : redHub
@@ -3138,7 +3389,8 @@ function formatVerify(result) {
3138
3389
 
3139
3390
  async function setupAgentWorkflow(root, flags) {
3140
3391
  const hubs = setupHubsFromFlags(flags);
3141
- const absoluteRoot = path.resolve(root || '.');
3392
+ const rootResolution = await resolveExistingSkillRoot(root, flags);
3393
+ const absoluteRoot = rootResolution.root;
3142
3394
  const outDir = path.resolve(flags['out-dir'] || path.join(absoluteRoot, WORK_DIR));
3143
3395
  await mkdir(outDir, { recursive: true });
3144
3396
  const manifestResult = await writeManifest(absoluteRoot, {
@@ -3236,6 +3488,8 @@ async function setupAgentWorkflow(root, flags) {
3236
3488
  return {
3237
3489
  ok: true,
3238
3490
  hubs,
3491
+ linkedSkillDiscovered: rootResolution.discovered,
3492
+ adoptedLegacyXiaSheManifest: manifestResult.adoptedLegacyXiaSheManifest,
3239
3493
  manifestPath: manifestResult.manifestPath,
3240
3494
  handoffPath,
3241
3495
  promptPaths: promptResults,
@@ -3250,6 +3504,12 @@ async function setupAgentWorkflow(root, flags) {
3250
3504
  warnings,
3251
3505
  publicProtocol,
3252
3506
  next: [
3507
+ rootResolution.discovered
3508
+ ? 'An existing local Skill connection was found automatically; no new registry entry was created.'
3509
+ : null,
3510
+ manifestResult.adoptedLegacyXiaSheManifest
3511
+ ? 'A compatible XiaShe local connection was reused after the registry origin and environment check; a separate AgentPie manifest was created.'
3512
+ : null,
3253
3513
  firstPartyPackageArtifact?.skipped
3254
3514
  ? `${PRODUCT_NAME} complete package upload was skipped (${firstPartyPackageArtifact.reason}).`
3255
3515
  : `${PRODUCT_NAME} complete package uploaded: ${path.relative(absoluteRoot, firstPartyPackageArtifact.artifactPath)} (${firstPartyPackageArtifact.publishStatus || 'queued'}).`,
@@ -3287,8 +3547,9 @@ async function main() {
3287
3547
  if (commandArgs[0] && !commandArgs[0].startsWith('--')) {
3288
3548
  root = commandArgs.shift();
3289
3549
  }
3290
- const flags = parseArgs(commandArgs);
3550
+ let flags = parseArgs(commandArgs);
3291
3551
  try {
3552
+ flags = applyRegistryOriginFlags(flags);
3292
3553
  if (command === 'inspect') {
3293
3554
  const result = await inspectSkill(root, flags);
3294
3555
  print(result, flags.json);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiashe/skill",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "xiashe-skill": "bin/xiashe-skill.mjs"