@safeskill/cli 0.2.1 → 0.2.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.2
4
+
5
+ - Added cross-region hub resolve fallback when the selected SafeSkill hub returns a server error.
6
+
3
7
  ## 0.2.1
4
8
 
5
9
  - Added Flocks agent support with `.flocks/plugins/skills` install paths.
package/README.md CHANGED
@@ -76,9 +76,18 @@ npx @safeskill/cli add safeskill://official/acme/code-review@1.2.0
76
76
  | `-y, --yes` | Skip all confirmation prompts |
77
77
  | `--all` | Install all skills to all agents without prompts |
78
78
 
79
+ ### Global Options
80
+
81
+ | Option | Description |
82
+ | ---------------------- | ------------------------------------------------------------------------------------------------------------ |
83
+ | `--region <cn|global>` | Use a specific SafeSkill service region for this run. Overrides `SAFESKILL_REGION`; otherwise auto-detected. |
84
+
79
85
  ### Examples
80
86
 
81
87
  ```bash
88
+ # Force the China service region for this run
89
+ npx @safeskill/cli --region cn add safeskill://official/acme/code-review
90
+
82
91
  # List skills in a repository
83
92
  npx @safeskill/cli add example-org/agent-skills --list
84
93
 
@@ -441,6 +450,7 @@ Ensure you have write access to the target directory.
441
450
  | Variable | Description |
442
451
  | ------------------------- | -------------------------------------------------------------------------- |
443
452
  | `INSTALL_INTERNAL_SKILLS` | Set to `1` or `true` to show and install skills marked as `internal: true` |
453
+ | `SAFESKILL_REGION` | Optional fallback service region (`cn` or `global`) when `--region` is not specified |
444
454
  | `DISABLE_TELEMETRY` | Set to disable anonymous usage telemetry |
445
455
  | `DO_NOT_TRACK` | Alternative way to disable telemetry |
446
456
 
package/dist/cli.mjs CHANGED
@@ -408,13 +408,18 @@ const STATUS_TIMEOUT_MS = 2500;
408
408
  const DETECT_TIMEOUT_MS = 1500;
409
409
  let resolvedRegionPromise = null;
410
410
  let regionAvailabilityCache = /* @__PURE__ */ new Map();
411
- function normalizeRegion(region) {
411
+ let safeSkillRegionOverride = null;
412
+ function normalizeSafeSkillRegion(region) {
412
413
  const value = region?.trim().toLowerCase();
413
414
  if (!value) return null;
414
415
  if (value === "cn" || value === "china") return "cn";
415
416
  if (value === "global" || value === "io" || value === "intl" || value === "international") return "global";
416
417
  return null;
417
418
  }
419
+ function setSafeSkillRegionOverride(region) {
420
+ safeSkillRegionOverride = region;
421
+ resolvedRegionPromise = null;
422
+ }
418
423
  function withTimeout(timeoutMs) {
419
424
  return AbortSignal.timeout(timeoutMs);
420
425
  }
@@ -496,7 +501,8 @@ async function ensureRegionAvailable(region, hubBaseUrl) {
496
501
  async function resolveSafeSkillRegion() {
497
502
  if (resolvedRegionPromise) return resolvedRegionPromise;
498
503
  resolvedRegionPromise = (async () => {
499
- const configuredRegion = normalizeRegion(process.env.SAFESKILL_REGION);
504
+ if (safeSkillRegionOverride) return safeSkillRegionOverride;
505
+ const configuredRegion = normalizeSafeSkillRegion(process.env.SAFESKILL_REGION);
500
506
  if (configuredRegion) return configuredRegion;
501
507
  return detectRegionByPublicIP();
502
508
  })();
@@ -530,6 +536,19 @@ function normalizeApiBase(baseUrl) {
530
536
  function buildApiUrl(baseUrl, path) {
531
537
  return `${normalizeApiBase(baseUrl)}${path.startsWith("/") ? path : `/${path}`}`;
532
538
  }
539
+ function getAlternateHubBaseUrl(baseUrl) {
540
+ if (process.env.SAFESKILL_HUB_URL) return null;
541
+ const parsed = new URL(baseUrl);
542
+ if (parsed.hostname === "hubapi.safeskill.io") {
543
+ parsed.hostname = "hubapi.safeskill.cn";
544
+ return parsed.toString().replace(/\/$/, "");
545
+ }
546
+ if (parsed.hostname === "hubapi.safeskill.cn") {
547
+ parsed.hostname = "hubapi.safeskill.io";
548
+ return parsed.toString().replace(/\/$/, "");
549
+ }
550
+ return null;
551
+ }
533
552
  async function readEnvelope(response) {
534
553
  const payload = await response.json();
535
554
  if (payload && typeof payload === "object" && "data" in payload) return payload.data;
@@ -724,26 +743,41 @@ async function searchSkillsCatalog(query, limit = 10) {
724
743
  }
725
744
  }
726
745
  async function resolveHubSkill(source, agent) {
727
- const resolveUrl = buildApiUrl(await getSafeSkillHubBaseUrl(), "/resolve");
728
- const resolveWithAgent = agent ? await fetch(resolveUrl, {
729
- method: "POST",
730
- headers: getHeaders({ "content-type": "application/json" }),
731
- body: JSON.stringify({
732
- source,
733
- agent
734
- })
735
- }) : null;
736
- const response = resolveWithAgent && resolveWithAgent.ok ? resolveWithAgent : await fetch(resolveUrl, {
737
- method: "POST",
738
- headers: getHeaders({ "content-type": "application/json" }),
739
- body: JSON.stringify({ source })
740
- });
746
+ const baseUrl = await getSafeSkillHubBaseUrl();
747
+ const resolveFromBaseUrl = async (hubBaseUrl) => {
748
+ const resolveUrl = buildApiUrl(hubBaseUrl, "/resolve");
749
+ const resolveWithAgent = agent ? await fetch(resolveUrl, {
750
+ method: "POST",
751
+ headers: getHeaders({ "content-type": "application/json" }),
752
+ body: JSON.stringify({
753
+ source,
754
+ agent
755
+ })
756
+ }) : null;
757
+ return resolveWithAgent && resolveWithAgent.ok ? resolveWithAgent : fetch(resolveUrl, {
758
+ method: "POST",
759
+ headers: getHeaders({ "content-type": "application/json" }),
760
+ body: JSON.stringify({ source })
761
+ });
762
+ };
763
+ let resolvedBaseUrl = baseUrl;
764
+ let response = await resolveFromBaseUrl(baseUrl);
765
+ const alternateBaseUrl = response.status >= 500 ? getAlternateHubBaseUrl(baseUrl) : null;
766
+ if (alternateBaseUrl) {
767
+ const alternateResponse = await resolveFromBaseUrl(alternateBaseUrl);
768
+ if (alternateResponse.ok || !response.ok) {
769
+ response = alternateResponse;
770
+ resolvedBaseUrl = alternateBaseUrl;
771
+ }
772
+ }
741
773
  if (!response.ok) throw new Error(`hub resolve failed: ${response.status}`);
742
- return readEnvelope(response);
774
+ const resolved = await readEnvelope(response);
775
+ resolved.hubBaseUrl = resolvedBaseUrl;
776
+ return resolved;
743
777
  }
744
778
  async function downloadHubSource(source, agent) {
745
- const baseUrl = await getSafeSkillHubBaseUrl();
746
779
  const resolved = await resolveHubSkill(source, agent);
780
+ const baseUrl = resolved.hubBaseUrl || await getSafeSkillHubBaseUrl();
747
781
  const downloadUrl = resolved.download_uri || (resolved.artifact_digest ? buildApiUrl(baseUrl, `/artifacts/${encodeURIComponent(resolved.artifact_digest)}/download`) : "");
748
782
  if (!downloadUrl) throw new Error("hub resolve did not return a downloadable artifact");
749
783
  let response;
@@ -901,6 +935,8 @@ async function parseSkillMd(skillMdPath, options) {
901
935
  ...metaData
902
936
  };
903
937
  } catch (metaError) {}
938
+ if (!skillData.name && options?.fallbackName) skillData.name = options.fallbackName;
939
+ if (!skillData.description && options?.fallbackDescription) skillData.description = options.fallbackDescription;
904
940
  if (!skillData.name || !skillData.description) return null;
905
941
  if (typeof skillData.name !== "string" || typeof skillData.description !== "string") return null;
906
942
  if (skillData.metadata?.internal === true && !shouldInstallInternalSkills() && !options?.includeInternal) return null;
@@ -910,7 +946,8 @@ async function parseSkillMd(skillMdPath, options) {
910
946
  path: dirname(skillMdPath),
911
947
  rawContent: content,
912
948
  metadata: skillData.metadata,
913
- slug: skillData.slug
949
+ slug: skillData.slug || options?.fallbackSlug,
950
+ installName: options?.fallbackInstallName
914
951
  };
915
952
  } catch {
916
953
  return null;
@@ -2278,7 +2315,7 @@ function createEmptyLocalLock() {
2278
2315
  skills: {}
2279
2316
  };
2280
2317
  }
2281
- var version$1 = "0.2.1";
2318
+ var version$1 = "0.2.3";
2282
2319
  const isCancelled$1 = (value) => typeof value === "symbol";
2283
2320
  async function isSourcePrivate(source) {
2284
2321
  const ownerRepo = parseOwnerRepo(source);
@@ -2793,10 +2830,24 @@ async function runAdd(args, options = {}) {
2793
2830
  }
2794
2831
  const includeInternal = !!(options.skill && options.skill.length > 0);
2795
2832
  spinner.start("Discovering skills...");
2796
- const skills = await discoverSkills(skillsDir, parsed.subpath, {
2833
+ let skills = await discoverSkills(skillsDir, parsed.subpath, {
2797
2834
  includeInternal,
2798
2835
  fullDepth: options.fullDepth
2799
2836
  });
2837
+ let usedHubMetadataFallback = false;
2838
+ if (skills.length === 0 && parsed.type === "hub" && !parsed.subpath && hubResolved?.skill) {
2839
+ const fallbackSkill = await parseSkillMd(join(skillsDir, "SKILL.md"), {
2840
+ includeInternal,
2841
+ fallbackName: hubResolved.skill.display_name || hubResolved.skill.name,
2842
+ fallbackDescription: hubResolved.skill.summary || hubResolved.skill.name,
2843
+ fallbackSlug: hubResolved.skill.name,
2844
+ fallbackInstallName: hubResolved.skill.name
2845
+ });
2846
+ if (fallbackSkill) {
2847
+ skills = [fallbackSkill];
2848
+ usedHubMetadataFallback = true;
2849
+ }
2850
+ }
2800
2851
  if (skills.length === 0) {
2801
2852
  spinner.stop(import_picocolors.default.red("No skills found"));
2802
2853
  Se(import_picocolors.default.red("No valid skills found. Skills require a SKILL.md with name and description."));
@@ -2804,6 +2855,7 @@ async function runAdd(args, options = {}) {
2804
2855
  process.exit(1);
2805
2856
  }
2806
2857
  spinner.stop(`Found ${import_picocolors.default.green(skills.length)} skill${skills.length > 1 ? "s" : ""}`);
2858
+ if (usedHubMetadataFallback) M.warn("Warning: this hub artifact is missing SKILL.md frontmatter. Some agents may not recognize this skill until name and description are added to SKILL.md.");
2807
2859
  if (options.list) {
2808
2860
  console.log();
2809
2861
  M.step(import_picocolors.default.bold("Available Skills"));
@@ -4369,6 +4421,8 @@ ${BOLD}List Options:${RESET}
4369
4421
  ${BOLD}Options:${RESET}
4370
4422
  --help, -h Show this help message
4371
4423
  --version, -v Show version number
4424
+ --region <cn|global>
4425
+ Use a specific SafeSkill service region for this run
4372
4426
 
4373
4427
  ${BOLD}Examples:${RESET}
4374
4428
  ${DIM}$${RESET} safeskill add example-org/agent-skills
@@ -4655,7 +4709,7 @@ async function runCheck(args = []) {
4655
4709
  });
4656
4710
  console.log();
4657
4711
  }
4658
- async function runUpdate() {
4712
+ async function runUpdate(region) {
4659
4713
  console.log(`${TEXT}Checking for skill updates...${RESET}`);
4660
4714
  console.log();
4661
4715
  const lock = readSkillLock();
@@ -4751,13 +4805,22 @@ async function runUpdate() {
4751
4805
  console.log(` ${DIM}✗ Failed to update ${update.name}: CLI entrypoint not found at ${cliEntry}${RESET}`);
4752
4806
  continue;
4753
4807
  }
4754
- if (spawnSync(process.execPath, [
4808
+ const addArgs = region ? [
4755
4809
  cliEntry,
4810
+ "--region",
4811
+ region,
4756
4812
  "add",
4757
4813
  installUrl,
4758
4814
  "-g",
4759
4815
  "-y"
4760
- ], {
4816
+ ] : [
4817
+ cliEntry,
4818
+ "add",
4819
+ installUrl,
4820
+ "-g",
4821
+ "-y"
4822
+ ];
4823
+ if (spawnSync(process.execPath, addArgs, {
4761
4824
  stdio: [
4762
4825
  "inherit",
4763
4826
  "pipe",
@@ -4784,8 +4847,53 @@ async function runUpdate() {
4784
4847
  });
4785
4848
  console.log();
4786
4849
  }
4850
+ function parseGlobalOptions(args) {
4851
+ const parsedArgs = [];
4852
+ let region;
4853
+ for (let i = 0; i < args.length; i++) {
4854
+ const arg = args[i];
4855
+ if (arg === void 0) continue;
4856
+ if (arg === "--region") {
4857
+ const value = args[i + 1];
4858
+ if (typeof value !== "string" || value.startsWith("-")) return {
4859
+ args: parsedArgs,
4860
+ error: "Missing value for --region. Expected one of: cn, global"
4861
+ };
4862
+ const normalized = normalizeSafeSkillRegion(value);
4863
+ if (!normalized) return {
4864
+ args: parsedArgs,
4865
+ error: `Invalid region: ${value}\nExpected one of: cn, global`
4866
+ };
4867
+ region = normalized;
4868
+ i++;
4869
+ continue;
4870
+ }
4871
+ if (arg.startsWith("--region=")) {
4872
+ const value = arg.slice(9);
4873
+ const normalized = normalizeSafeSkillRegion(value);
4874
+ if (!normalized) return {
4875
+ args: parsedArgs,
4876
+ error: `Invalid region: ${value}\nExpected one of: cn, global`
4877
+ };
4878
+ region = normalized;
4879
+ continue;
4880
+ }
4881
+ parsedArgs.push(arg);
4882
+ }
4883
+ return {
4884
+ args: parsedArgs,
4885
+ region
4886
+ };
4887
+ }
4787
4888
  async function main() {
4788
- const args = process.argv.slice(2);
4889
+ const parsedGlobalOptions = parseGlobalOptions(process.argv.slice(2));
4890
+ if (parsedGlobalOptions.error) {
4891
+ console.log(parsedGlobalOptions.error);
4892
+ process.exitCode = 1;
4893
+ return;
4894
+ }
4895
+ if (parsedGlobalOptions.region) setSafeSkillRegionOverride(parsedGlobalOptions.region);
4896
+ const args = parsedGlobalOptions.args;
4789
4897
  if (args.length === 0) {
4790
4898
  showBanner();
4791
4899
  return;
@@ -4844,7 +4952,7 @@ async function main() {
4844
4952
  break;
4845
4953
  case "update":
4846
4954
  case "upgrade":
4847
- runUpdate();
4955
+ runUpdate(parsedGlobalOptions.region);
4848
4956
  break;
4849
4957
  case "--help":
4850
4958
  case "-h":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@safeskill/cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "SafeSkill CLI, for safe your skills",
5
5
  "type": "module",
6
6
  "bin": {